문제

We have a customer that wishes to make it easier for the user to customize their webpart pages. We are therefor using javascript to build a simple grid where users can add, remove and move Add In Parts (both webparts and AppParts and both OOTB and custom) from the different webpart zones on the current page. Most of the features are working fine but we have two problems we can't seem to find the answer to. In this case we are working with a Farm Solution but we want it to work with Office365 as well.

  1. Is Add In Part shared or personal?

We want the users to be able to remove/hide Add In Parts they don't want and we are doing that with SP.WebParts.WebPartDefinition.deleteWebPart(). This works fine for Add In Parts the user has added to his/her personal site but if you try this with a shared Add In Part you get an error. We would like to find out if the Add In Part is shared and in that case set hidden=true. We can't seem to find anything about how to check if the Add In Part is shared or not other then to first try "deleteWebPart()" and if it gives an error try "hidden=true" instead.

  1. Get ClientWebPart (AppPart) xml

To be able to add an Add In Part to the page from the gallery you need it's xml in this method: importWebPart(). For webparts we can find the .webpart-files by checking the items in "~siteUrl/_api/web/getCatalog(113)/items" but AppParts does not exist in this list. To get all Apps we are using SP.AppCatalog.getAppInstances(context, web) and from this we get each apps name, guid etc but not it's xml. Where can we find that? Everywhere people asking the same are advised to just export the xml manually and use it in the code but that is not good enought. This needs to be dynamic and work with all AppParts.

Thank you for your help.

도움이 되었습니까?

해결책

I acually found some people at my company that had made a similar function and together with them I was able to find a somewhat satisfying solution. If anyone else in the future want to do the same thing as this I though I would share how I've done this.

Question 1: Is Add In Part shared or personal

I could not find any property on an AddInPart for if it's shared or created by the user so I solved this by simply loading all shared AddInParts and stored thier GUIDs in an array. By doing so I could check whether the AddInPart i wanted to remove was in that array or not and in that case take the correct action. This is how I did it:

var sharedWebPartGuids = new Array(); // An array containing guids from all shared webparts

function fetchSharedWebparts() {
  //get the client context
  var clientContext = new SP.ClientContext(_spPageContextInfo.webServerRelativeUrl);
  //get the current page as a file
  var oFile = clientContext.get_web().getFileByServerRelativeUrl(_spPageContextInfo.serverRequestPath);
  // Fetch all webparts that are shared
  var limitedWebPartManagerShared = oFile.getLimitedWebPartManager(SP.WebParts.PersonalizationScope.shared);
  //get the web parts on the current page
  var collWebPart = limitedWebPartManagerUser.get_webParts();
  //request the web part collection and load it from the server
  clientContext.load(collWebPart);
  clientContext.executeQueryAsync(Function.createDelegate(this, function() {
    // Go through all webparts
    for (var x = 0; x < collWebPart.get_count(); x++) {
      var webPartDef = collWebPart.get_item(x);
      sharedWebPartGuids.push(webPartDef.get_id().toString());
    }
  }), Function.createDelegate(this, function() {
    alert("failed to fetch shared webparts from page")
  }));
}

Question 2: Get ClientWebPart (AppPart) xml

Unfortunetley I was not able to do this without manually providing the ProductID (a GUID) for each App that has an AppPart that should be able to be added. For this I deploy a list called AvailableAppParts which contains two columns; Title and ProductId. I then match the items in the list with the installed Apps by comparing Title. The GUID can be found in AppManifest.xml in the App or adding the AppPart to a page and then export it. I also had to include a xml template in my code and replace the unique values. Since everything is done async I have a counter called initMethods wich I add to for each async method and then at the end of them call initMethodDone() which checks if all async methods are done before I show anything to the user. This is how I did it:

var webpartsFromGallery = new Array(); // An array where I store an object for each AppPart
// The xml template:
var appPartXml = '<webParts><webPart xmlns="http://schemas.microsoft.com/WebPart/v3"><metaData><type name="Microsoft.SharePoint.WebPartPages.ClientWebPart, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" /><importErrorMessage>Cannot import this Web Part.</importErrorMessage> </metaData><data><properties><property name="TitleIconImageUrl" type="string" /><property name="Direction" type="direction">NotSet</property><property name="ExportMode" type="exportmode">NonSensitiveData</property><property name="HelpUrl" type="string" /><property name="Hidden" type="bool">False</property><property name="Description" type="string">AppPart Description</property><property name="FeatureId" type="System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">{FeatureId}</property><property name="Title" type="string">{Title}</property><property name="AllowHide" type="bool">True</property><property name="ProductWebId" type="System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">{ProductWebId}</property><property name="AllowZoneChange" type="bool">True</property><property name="TitleUrl" type="string" /><property name="ChromeType" type="chrometype">Default</property><property name="AllowConnect" type="bool">True</property><property name="Width" type="unit" /><property name="Height" type="unit" /><property name="WebPartName" type="string">AppPart</property><property name="HelpMode" type="helpmode">Navigate</property><property name="AllowEdit" type="bool">True</property><property name="AllowMinimize" type="bool">True</property><property name="ProductId" type="System.Guid, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">{ProductId}</property><property name="AllowClose" type="bool">True</property><property name="ChromeState" type="chromestate">Normal</property></properties></data></webPart></webParts>';

function fetchAppPartsFromGallery() {
  initMethods++;
  var ctx = SP.ClientContext.get_current();
  var web = ctx.get_web();
  // Fetch all installed Apps on this site (SPWeb)
  var appInstances = SP.AppCatalog.getAppInstances(ctx, web);
  ctx.load(appInstances);
  ctx.executeQueryAsync(
    function() {
      if (appInstances.get_count() == 0) {
        // No apps found, stop here
        return;
      }
      // Iterate through the app instances and store them in a map
      // Also create a filter string
      var map = new Object();
      var filterString = "";
      for (var i = 0; i < appInstances.get_count(); i++) {
        var instance = appInstances.getItemAtIndex(i);
        var title = instance.get_title();
        map[title] = instance;

        // Append filter
        if (filterString.length > 0) {
          filterString += "or";
        }
        filterString += "(Title+eq+'" + title + "')";
      }
      if (filterString.length > 0) {
        filterString = "?$filter=" + filterString;
      }

      // Fetch available apps from my list
      $.ajax({
        url: siteApiUrl + "/web/lists/getbytitle('AvailableAppParts')/items" + filterString,
        type: "GET",
        headers: {
          "accept": "application/json;odata=verbose",
        },
        success: function(data) {
          // Go through all returned apps and build the xml for each one
          var results = data.d.results;
          for (var i = 0; i < results.length; i++) {
            var listItem = results[i];
            var title = listItem.Title;
            var productId = listItem.ProductId;

            var appInstance = map[title];
            if (!appInstance) {
              console.log("Could not find app with title '" + title + "'");
              continue;
            }
            var instanceId = appInstance.get_id();
            var webId = appInstance.get_webId();
            var featureId = calculateFeatureIdFromProductId(productId);

            // This is the values we have to replace with our unique ones
            var xml = appPartXml.replace(/{Title}/g, title);
            xml = xml.replace(/{ProductId}/g, productId);
            xml = xml.replace(/{ProductWebId}/g, webId);
            xml = xml.replace(/{FeatureId}/g, featureId);

            webpartsFromGallery.push({
              id: instanceId,
              title: title,
              xml: xml
            });
          }

          function calculateFeatureIdFromProductId(productID) {
            // Calculate featureId since it is always one hexadecimal value higher than productId
            var lastIndex = productID.lastIndexOf('-');
            var lastNumber = productID.substring(lastIndex + 12);
            // We are using hexadecimal numbers therefor using parseInt(..., 16) and toString(16)
            var increment = parseInt(lastNumber, 16) + 1;
            var incrementHex = increment.toString(16);
            var featureID = productID.substring(0, lastIndex + 12) + incrementHex;
            return featureID;
          }

          initMethodDone();
        },
        error: function(error) {
          alert(JSON.stringify(error));

          initMethodDone();
        }

      });
    },
    function(sender, args) {
      console.log(JSON.stringify(args));

      initMethodDone();
    });
}
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 sharepoint.stackexchange
scroll top