projectdoc Toolbox

Renders a menu with tools to inspect information from a projectdoc document, shown in the browser.

Tags
Identifier
de.smartics.userscripts.confluence.projectdoc-inspect-menu
Repository
Since
1.0

The userscript renders a menu on a Confluence page with tools to inspect the projectdoc document shown in the browser.

It implements the following actions:

  1. Display Document Properties
  2. Display Space Properties
  3. Display Transcluding Documents

Code

The code of the script for reference.

projectdoc-inspect-menu.js
/*
 * Copyright 2019-2020 Kronseder & Reiner GmbH, smartics
 *
 * 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.
 */

"use strict";

AJS.toInit(function () {
  const logToConsole = false;

  const $propertiesMarker = AJS.$(".projectdoc-document-element.properties");
  if (!$propertiesMarker.length) {
    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Not a projectdoc document. Quitting.");
    return;
  }

  const showDocumentProperties = function () {
    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Fetching document properties ...");
    const pageId = AJS.params.pageId;
    const locale = AJS.params.userLocale;
    const baseURL = AJS.params.baseUrl;

    const htmlTitle = "Page Properties";
    let html = "<html lang='" + locale + "'><head><title>" + htmlTitle + "</title><style>" +
      " .table-sm td, .table-sm th {padding: .1rem !important;}" +
      " .table td, .table th { font-size: 10px !important;}" +
      "</style>" +
      "<link rel='stylesheet'" +
      " href='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css'" +
      " integrity='sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T'" +
      " crossorigin='anonymous'>" +
      "</head><body><h6>" + htmlTitle + "</h6>";
    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Making call ...");
    AJS.$.ajax({
      url: baseURL + "/rest/projectdoc/1/document/" + pageId + ".json?expand=property&resource-mode=html",
      async: true,
      contentType: 'application/json'
    }).success(function (data) {
      if (logToConsole) AJS.log("[projectdoc-inspect-menu] Document Properties Data: " + JSON.stringify(data));

      html += "<table class='table table-sm table-bordered table-striped'>";
      AJS.$.each(data["property"],
        function (index, obj) {
          html = html + "<tr><th>" + obj.name + "</th><td>" + obj.value + "</td></tr>"
        }
      );
      html += "</table></body></html>";
      const showDialog = window.open('', '', 'width=600,height=800,location=no,toolbar=0');
      showDialog.document.body.innerHTML = html;
    }).error(function (jqXHR, textStatus) {
      AJS.log("[projectdoc-inspect-menu] Error fetching document properties: " + jqXHR.status + " (" + textStatus + ")");
      alert("Failed to fetch document properties: " + jqXHR.status + " (" + textStatus + ")");
    });
  };

  const showSpaceProperties = function () {
    const spaceKey = AJS.params.spaceKey;
    const locale = AJS.params.userLocale;
    const baseUrl = AJS.params.baseUrl;

    const htmlTitle = "Space Properties for " + spaceKey;
    const htmlStart = "<html lang='" + locale + "'><head><title>" + htmlTitle + "</title><style>" +
      " .table-sm td, .table-sm th {padding: .1rem !important;}" +
      " .table td, .table th { font-size: 10px !important;}" +
      "</style>" +
      "<link rel='stylesheet'" +
      " href='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css'" +
      " integrity='sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T'" +
      " crossorigin='anonymous'>" +
      "</head><body>";
    const htmlEnd = "</body></html>";

    const $body = AJS.$('<div></div>');
    const $heading = AJS.$("<h6></h6>");
    $heading.text(htmlTitle);
    $body.append($heading);

    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Querying Properties Data for Space '" + spaceKey + "' ...");
    AJS.$.ajax({
      url: baseUrl + "/rest/projectdoc/1/space/" + spaceKey,
      async: true,
      dataType: 'json'
      //contentType: 'application/json'
    }).success(function (data) {
      if (logToConsole) AJS.log("[projectdoc-inspect-menu] Space Properties Data: " + JSON.stringify(data));

      const $table = AJS.$('<table class="table table-sm table-bordered table-striped"></table>')
      AJS.$.each(data["property"],
        function (index, obj) {
          const $tr = AJS.$("<tr></tr>");
          const $tdSource = AJS.$("<td></td>");
          $tdSource.text(obj.source);
          $tr.append($tdSource);

          const $th = AJS.$("<th></th>");
          $th.text(obj.name);
          $tr.append($th);

          const $tdValue = AJS.$("<td></td>");
          $tdValue.text(obj.value);
          AJS.log("VALUE: " + obj.value);
          $tr.append($tdValue);

          $table.append($tr);
        }
      );
      $body.append($table);

      const html = htmlStart + $body.html() + htmlEnd;
      const showDialog = window.open('', '', 'width=600,height=800,location=no,toolbar=0');
      showDialog.document.body.innerHTML = html;
    }).error(function (jqXHR, textStatus) {
      AJS.log("[projectdoc-inspect-menu] Error fetching space properties: " + jqXHR.status + " (" + textStatus + ")");
      alert("Failed to fetch space properties: " + jqXHR.status + " (" + textStatus + ")");
    });
  };

  const listTranscludingDocument = function () {
    const pageId = AJS.params.pageId;
    const locale = AJS.params.userLocale;
    const baseUrl = AJS.params.baseUrl;

    function createPage(i18n, currentDocument, transclusionDocuments, delegateDocuments, dynamicLinkTitlesDocuments, doctypeNameReferencesDocuments) {
      const htmlTitle = "Transcluding Documents for " + currentDocument[i18n.name];
      let html = "<html lang='" + locale + "'><head><title>" + htmlTitle + "</title><style>" +
        " body {margin: 1rem !important;}" +
        " .table-sm td, .table-sm th {padding: .1rem !important;}" +
        " .table td, .table th { font-size: .8rem !important;}" +
        "</style>" +
        "<link rel='stylesheet'" +
        " href='https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css'" +
        " integrity='sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T'" +
        " crossorigin='anonymous'>" +
        "</head><body><h3>" + htmlTitle + "</h3>";
      const transclusionHitCount = transclusionDocuments.document.length;
      const tinyUrlNamePlain = i18n["tinyUrl"] + '\u00a7';
      html += "<p>Content of document '<a href='" + currentDocument[tinyUrlNamePlain] + "'>" + currentDocument[i18n.name] + "</a>' is transcluded by ";
      if (transclusionHitCount === 0) {
        html += "no document.</p>";
      } else if (transclusionHitCount === 1) {
        html += "one document.</p>";
      } else {
        html += transclusionHitCount + " documents.</p>";
      }

      if (transclusionHitCount > 0) {
        html += "<table class='table table-sm table-bordered table-striped'>";
        AJS.$.each(transclusionDocuments.document, function (index, doc) {
            const current = {};
            AJS.$.each(doc.property, function (i, property) {
              current[property.name] = property.value;
            });
            const name = current[i18n.name];
            const shortDescription = current[i18n.shortDescription];
            const url = current[tinyUrlNamePlain];
            html = html + "<tr><th><a href='" + url + "'>" + name + "</a></th><td>" + shortDescription + "</td></tr>";
          }
        );
        html += "</table>";
      }

      const delegateHitCount = delegateDocuments.document.length;
      html += "<p>The document is delegate of ";
      if (delegateHitCount === 0) {
        html += "no document.</p>";
      } else if (delegateHitCount === 1) {
        html += "one document.</p>";
      } else {
        html += delegateHitCount + " documents.</p>";
      }

      if (delegateHitCount > 0) {
        html += "<table class='table table-sm table-bordered table-striped'>";
        AJS.$.each(delegateDocuments.document, function (index, doc) {
            const current = {};
            AJS.$.each(doc.property, function (i, property) {
              current[property.name] = property.value;
            });
            const name = current[i18n.name];
            const shortDescription = current[i18n.shortDescription];
            const url = current[tinyUrlNamePlain];
            html = html + "<tr><th><a href='" + url + "'>" + name + "</a></th><td>" + shortDescription + "</td></tr>";
          }
        );
        html += "</table>";
      }

      const dynamicLinksHitCount = dynamicLinkTitlesDocuments.document.length;
      html += "<p>The document is a possible target for dynamic links in ";
      if (dynamicLinksHitCount === 0) {
        html += "no documents.</p>";
      } else if (dynamicLinksHitCount === 1) {
        html += "one document.</p>";
      } else {
        html += dynamicLinksHitCount + " documents.</p>";
      }

      if (dynamicLinksHitCount > 0) {
        html += "<table class='table table-sm table-bordered table-striped'>";
        AJS.$.each(dynamicLinkTitlesDocuments.document, function (index, doc) {
            const current = {};
            AJS.$.each(doc.property, function (i, property) {
              current[property.name] = property.value;
            });
            const name = current[i18n.name];
            const shortDescription = current[i18n.shortDescription];
            const url = current[tinyUrlNamePlain];
            html = html + "<tr><th><a href='" + url + "'>" + name + "</a></th><td>" + shortDescription + "</td></tr>";
          }
        );
        html += "</table>";
      }

      const doctypeNameReferencesHitCount = doctypeNameReferencesDocuments.document.length;
      html += "<p>The document is a possible target for Doctype/Name reference in ";
      if (doctypeNameReferencesHitCount === 0) {
        html += "no documents.</p>";
      } else if (doctypeNameReferencesHitCount === 1) {
        html += "one document.</p>";
      } else {
        html += doctypeNameReferencesHitCount + " documents.</p>";
      }

      if (doctypeNameReferencesHitCount > 0) {
        html += "<table class='table table-sm table-bordered table-striped'>";
        AJS.$.each(doctypeNameReferencesDocuments.document, function (index, doc) {
            const current = {};
            AJS.$.each(doc.property, function (i, property) {
              current[property.name] = property.value;
            });
            const name = current[i18n.name];
            const shortDescription = current[i18n.shortDescription];
            const url = current[tinyUrlNamePlain];
            html = html + "<tr><th><a href='" + url + "'>" + name + "</a></th><td>" + shortDescription + "</td></tr>";
          }
        );
        html += "</table>";
      }

      html += "</body></html>";
      return html;
    }

    if (PDBMLS) {
      const i18n = PDBMLS.fetchI18n(baseUrl, ["title", "spaceKey", "doctype", "name", "shortDescription", "tinyUrl", "projectdoc.doctype.common.delegateDocument.pageRef", "projectdoc.doctype.common.metadata.dynamicLinkTitles", "projectdoc.doctype.common.metadata.documentDoctypeNameReferences"]);
      const tinyUrlNamePlain = i18n["tinyUrl"] + '\u00a7';

      const currentDocument = PDBMLS.fetchDocument(baseUrl, pageId, [i18n.spaceKey, i18n.title, i18n.doctype, i18n.name, tinyUrlNamePlain]);

      if (currentDocument) {
        const spaceKey = currentDocument[i18n.spaceKey];
        const title = currentDocument[i18n.title];
        const pageReference = spaceKey + "." + title;
        const whereTransclusion = "$<TranscludedDocumentTitles>=[" + pageReference + "]";

        const tableDataPropertyNames = [i18n.name, i18n.shortDescription, tinyUrlNamePlain];
        const transcludingDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereTransclusion);

        const delegatePageRefName = i18n["projectdoc.doctype.common.delegateDocument.pageRef"];
        const whereDelegate = "$<" + delegatePageRefName + ">=[" + pageReference + "]";
        const delegateDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereDelegate);

        const dynamicLinkTitles = i18n["projectdoc.doctype.common.metadata.dynamicLinkTitles"];
        const whereDynamicLinkTitles = "$<" + dynamicLinkTitles + ">=[" + title + "]";
        // AJS.log("Where (Dynamic Link Title): " + whereDynamicLinkTitles);
        const dynamicLinkTitlesDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereDynamicLinkTitles);

        let doctypeNameReferencesDocuments;
        const doctypeNameReferencesName = i18n["projectdoc.doctype.common.metadata.documentDoctypeNameReferences"];
        if (doctypeNameReferencesName) {
          const doctype = currentDocument[i18n.doctype];
          const name = currentDocument[i18n.name];
          const documentReference = doctype + ":" + name;
          const whereDoctypeNameReferences = "$<" + doctypeNameReferencesName + ">=[" + documentReference + "]";
          AJS.log("[projectdoc-inspect-menu] Where: " + whereDoctypeNameReferences);
          doctypeNameReferencesDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereDoctypeNameReferences);
        }

        let html = createPage(i18n, currentDocument, transcludingDocuments, delegateDocuments, dynamicLinkTitlesDocuments, doctypeNameReferencesDocuments);

        const container = window.open('', '', 'width=600,height=800,location=no,toolbar=0');
        container.document.body.innerHTML = html;
      }
    } else {
      AJS.log("[projectdoc-inspect-menu] Error transcluding documents. PDBMLS service of Bookmarklets Extension not found.");
      alert("Failed to transcluding documents: PDBMLS service of Bookmarklets Extension not found.");
    }
  };

  const createMenu = function () {
    const menuId = "inspect";
    const propertiesSectionId = "projectdoc-inspect-menu-properties";

    const $mainMenu = USERSCRIPT4C_MENU.createMenu(menuId, "Inspect");
    USERSCRIPT4C_MENU.registerMenu("view.menu", $mainMenu);
    USERSCRIPT4C_MENU.addSection(menuId, {
      id: propertiesSectionId,
      label: "Properties",
      weight: 10
    });

    USERSCRIPT4C_MENU.addMenuItem(propertiesSectionId, {
      id: "projectdoc-menu-inspect-item-document-properties",
      label: "Show document properties",
      weight: "100"
    }, showDocumentProperties);
    USERSCRIPT4C_MENU.addMenuItem(propertiesSectionId, {
      id: "projectdoc-menu-inspect-item-space-properties",
      label: "Show space properties",
      weight: "200"
    }, showSpaceProperties);
    USERSCRIPT4C_MENU.addMenuItem(propertiesSectionId, {
      id: "projectdoc-menu-inspect-item-transclusions",
      label: "Show transclusions",
      weight: "300"
    }, listTranscludingDocument);

    return createMenu;
  }

  if (logToConsole) AJS.log("[projectdoc-inspect-menu] Adding menu ...");
  createMenu();
});

Details

More information on using this userscript.

Rendering

The inspect menu is rendered next to the create button in the Confluence toolbar.

Screenshot showing the menu with its menu items.

Requirements

The script requires the following apps to be installed on Confluence.

The projectdoc Toolbox for Atlassian Confluence
The projectdoc Toolbox supports agile teams in writing project documentation collaboratively. This is an introduction to use cases for and features of the projectdoc Toolbox.
Web API Extension
Add-on to extend projectdoc with an API to access on the web.
Bookmarklets Extension
Add-on to extend the Toolkit with Bookmarklets. Allows to execute tools via the browser.

Transcluding Documents Information

This version lists documents that

  1. transclude from the current document
  2. delegate to the current document

Only Static Transclusions

 

Please note that only static transclusions are listed in this report. A static transclusion is content from a referenced document. The Transclusion Macro uses static transclusion.

Dynamic transclusions are based on document queries. These transclusions are not listed in the report. The Transclude Documents Macro uses dynamic transclusion.

Related Scripts

Name Short Description
Removes projectdoc tools (blueprints and macros) from the current page.
Provides an interface to specify and launch queries for projectdoc documents.
Adds a refactor menu and checks the current document for property issues.

Resources

More information on this topic is available by the following resources.

Display Document Properties
Displays the document properties of the projectdoc document currently shown in the browser.
Display Space Properties
Displays the space properties of the projectdoc document's space currently shown in the browser.
List Transcluding Documents
Shows the list of documents that transclude content from the current document.
projectdoc Dynamic Link Titles
Lists the titles of all pages targeted by dynamic links.