projectdoc Toolbox

Provides an interface to specify and launch queries for projectdoc documents.

Tags
Identifier
de.smartics.userscripts.confluence.projectdoc-search-tool
Type
Repository
Since
1.0

Opens a dialog to specify a query to find projectdoc documents.

The dialog allows to select properties to show in the result set table (select), specify the spaces to include into the search (from), and add constraints for the matching document (where).

Code

The code of the script for reference.

projectdoc-search-tool.js
/*
 * Copyright 2019-2024 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 = USERSCRIPT4C.isVerboseLoggingRequestedFor('projectdoc-search-tool');
  const baseUrl = AJS.Meta.get('base-url');

  function log(message) {
    AJS.log("[projectdoc-search-tool] " + message);
  }

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

  if (logToConsole) log("Opening search ...");

  const createField = function (id, label, placeholder, description, isRequired) {
    const $group = AJS.$("<div class='field-group'></div>");
    const $label = AJS.$("<label></label>");
    $label.text(label);
    $label.attr("for", id);
    if (isRequired) {
      const $requiredSpan = AJS.$("<span class='aui-icon icon-required'>(required)</span>");
      $label.append($requiredSpan);
    }
    $group.append($label);

    const $input = AJS.$("<input class='text long-field' type='text'></input>");
    $input.attr("id", id);
    $input.attr("name", id);
    $input.attr("placeholder", placeholder);
    $group.append($input);

    if (description) {
      const $descriptionWrapper = AJS.$("<div class='description'></div>");
      $descriptionWrapper.append(description);
      $group.append($descriptionWrapper);
    }

    return $group;
  }

  const createTextarea = function (id, label, placeholder, description, isRequired) {
    const $fieldSet = AJS.$("<fieldset></fieldset>");
    $fieldSet.attr("id", id + "-component");

    const $legend = AJS.$("<legend></legend>");
    const $legendSpan = AJS.$("<span></span>");
    $legendSpan.text(description);
    $legend.append($legendSpan);
    $fieldSet.append($legend);

    const $group = AJS.$("<div class='field-group'></div>");
    $fieldSet.append($group);

    const $label = AJS.$("<label></label>");
    const textAreaId = id;
    $label.text(label);
    $label.attr("for", textAreaId);
    if (isRequired) {
      const $requiredSpan = AJS.$("<span class='aui-icon icon-required'>(required)</span>");
      $label.append($requiredSpan);
    }
    $group.append($label);

    const $textarea = AJS.$("<textarea></textarea>");
    $textarea.attr("id", textAreaId);
    $textarea.attr("name", textAreaId);
    $textarea.attr("placeholder", placeholder);
    $textarea.addClass("textarea long-field");
    /* Does not work.
    if(isRequired) {
      $textarea.attr("minlength", 3);
      $textarea.attr("data-aui-validation-field", "");
    }*/
    $group.append($textarea);

    return $fieldSet;
  };

  const removeParts = function ($searchResults) {
    AJS.$(".error").remove();
    $searchResults.children().remove();
    AJS.$("#projectdoc-messages").children().remove();
  }

  const createControls = function (documentIdsString) {
    const $controls = AJS.$("<div class='buttons'></div>");

    const $copyButton = AJS.$('<button id="copy-to-clipboard-button" class="aui-button" title="' + "Copy search result (document IDs) to clipboard ..." + '">' +
      '  <i class="aui-icon aui-icon-small aui-iconfont-copy"></i> ' + "Copy IDs" + '</button>');
    AJS.$($copyButton).on('click', function (e) {
      e.preventDefault();
      const tmp = document.createElement('textarea');
      tmp.value = documentIdsString;
      tmp.style = {position: 'absolute', left: '-9999px'};
      tmp.setAttribute('readonly', '');
      document.body.appendChild(tmp);
      tmp.select();
      document.execCommand('copy');
      document.body.removeChild(tmp);
    });
    $controls.append($copyButton);

    return $controls;
  };

  const createSearchForm = function (i18n, $searchResults) {
    const $form = AJS.$("<form class='aui' id='projectdoc-search-form'></form>");
    const $selectField = createField("select", "Select", "Property names to show in columns", undefined, false);
    $form.append($selectField);

    const $fromField = createField("from", "From", "Specify space keys", undefined, false);
    $form.append($fromField);

    const $whereField = createTextarea("where", "Where", "Query constraints", undefined, true);
    $form.append($whereField);

    const $sortByField = createField("sortBy", "Sort By", "Specify the sort criteria", undefined, false);
    $form.append($sortByField);

    const $controls = AJS.$("<div class='buttons-container'></div>");
    $form.append($controls);

    const $buttons = AJS.$("<div class='buttons'></div>");
    $controls.append($buttons);
    const $submit = AJS.$("<input class='button submit' type='submit' value='Find' id='comment-save-button'>");
    $submit.attr("title", "Run query to find matching document ...");

    AJS.$($submit).on('click', function (e) {
      e.preventDefault();
      const baseUrl = AJS.Meta.get('base-url');
      removeParts($searchResults);

      const selectValue = AJS.$.trim(AJS.$("#select").val());

      const nameName = i18n['name'];
      const shortDescriptionName = i18n['shortDescription'];
      const selectResolvedValue = selectValue && selectValue.length > 0 ? selectValue : nameName + ", " + shortDescriptionName;
      const select = encodeURIComponent(selectResolvedValue);

      const fromSpaceKeys = AJS.$.trim(AJS.$("#from").val());
      const from = encodeURIComponent(fromSpaceKeys && fromSpaceKeys.length > 0 ? fromSpaceKeys : "@all");
      const whereResolvedValue = AJS.$.trim(AJS.$("#where").val());
      if (whereResolvedValue) {
        const where = encodeURIComponent(whereResolvedValue);
        const sortBy = AJS.$.trim(AJS.$("#sortBy").val());
        const $spinner = AJS.$("<aui-spinner size='large'></aui-spinner>");
        $searchResults.append($spinner);
        const serviceUrl = baseUrl + "/rest/projectdoc/1/document.json?select=" + select + "&max-result=9999&from=" + from + "&where=" + where + "&expand=property,section&resource-mode=html" + (sortBy ? "&sort-by=" + encodeURIComponent(sortBy) : "");
        if (logToConsole) log("Service URL: " + serviceUrl);
        AJS.$.ajax({
          url: serviceUrl,
          async: true,
          contentType: 'application/json'
        }).success(function (data) {
          if (logToConsole) log("Documents: " + JSON.stringify(data));

          const size = data["size"];
          if (size > 0) {
            const $table = AJS.$("<table></table>");
            $table.addClass("aui");
            const $thead = AJS.$("<thead></thead>");
            $table.append($thead);
            const $theadTr = AJS.$("<tr></tr>");
            $thead.append($theadTr);
            const selectedPropertyNames = selectResolvedValue.split(/,\s?/);
            AJS.$.each(selectedPropertyNames,
              function (index, label) {
                const $th = AJS.$("<th></th>");
                $th.attr("id", "projectdoc-search-column-" + index);
                $th.text(label);
                $theadTr.append($th);
              });
            const $tbody = AJS.$("<tbody></tbody>");
            $table.append($tbody);
            const documentIdsString = data["id-list"];
            AJS.$.each(data["document"],
              function (documentIndex, document) {
                const documentId = document['id'];
                const properties = document["property"];
                const sections = document["section"];

                const $row = AJS.$("<tr></tr>");
                AJS.$.each(selectedPropertyNames,
                  function (index, currentPropertyName) {
                    const $value = AJS.$("<td></td>");
                    let found = false;
                    if (properties.length > 0) {
                      AJS.$.each(properties, function (propertyIndex, property) {
                        if (currentPropertyName === property.name) {
                          if (nameName !== property.name) {
                            $value.html(property.value);
                          } else {
                            $value.html("<a href='" + baseUrl + "/pages/viewpage.action?pageId=" + documentId + "' target='_blank'>" + property.value + "</a>");
                          }

                          $value.attr("headers", "projectdoc-search-column" + propertyIndex);
                          found = true;
                        }
                      });
                    }

                    if (!found && sections.length > 0) {
                      AJS.$.each(sections, function (sectionIndex, section) {
                        if (currentPropertyName === section.title) {
                          found = true;
                          const sectionString = section.content;
                          if (sectionString) {
                            $value.html(sectionString);
                          }
                          $value.attr("headers", "projectdoc-search-column" + sectionIndex);
                        }
                      });
                    }

                    $row.append($value);
                  }
                );
                $tbody.append($row);
              }
            );
            $spinner.remove();
            $searchResults.append(AJS.$("<p>Found " + (size > 1 ? size + " matching documents" : "1 matching document") + ".</p>"))
            if (size > 0) {
              const $controls = createControls(documentIdsString);
              $searchResults.append($controls);
            }
            $searchResults.append($table);
          } else {
            $spinner.remove();
            const $p = AJS.$("<p></p>");
            $p.text("No results.");
            $searchResults.append($p);
          }
        }).error(function (jqXHR, textStatus) {
          $spinner.remove();
          log("Error fetching documents: " + jqXHR.status + " (" + textStatus + ")");
          showMessage("Error", "Failed to fetch documents for query: " + jqXHR.status + " (" + textStatus + ")", "error");
        });
      } else {
        const $errorMessage = AJS.$("<div class='error'></div>");
        $errorMessage.text("Constraint is required to not retrieve all documents per accident.");
        AJS.$("#where").parent().append($errorMessage);
      }
    });
    $buttons.append($submit);

    const $clearForm = AJS.$("<a class='cancel' href='#'>Clear Form</a>");
    $clearForm.attr("title", "Clear all data from search form ...");
    AJS.$($clearForm).on('click', function (e) {
      e.preventDefault();
      AJS.$("#select").val("");
      AJS.$("#from").val("");
      AJS.$("#where").val("");
      AJS.$(".error").remove();
    });
    $buttons.append($clearForm);

    const $clearResults = AJS.$("<a class='cancel' href='#'>Clear Results</a>");
    $clearResults.attr("title", "Remove table of matching documents ...");
    AJS.$($clearResults).on('click', function (e) {
      e.preventDefault();
      $searchResults.children().remove();
      AJS.$("#projectdoc-messages").children().remove();
    });
    $buttons.append($clearResults);

    return $form;
  };

  const showMessage = function (title, content, type) {
    const $messages = AJS.$("#projectdoc-messages");
    if ($messages.length) {
      const $message = AJS.$("<div></div>");
      $message.addClass("aui-message aui-message-" + type);
      const $title = AJS.$("<p></p>");
      $title.addClass("title");
      const $titleSpan = AJS.$("<strong></strong>");
      $titleSpan.text(title);
      $title.append($titleSpan);
      $message.append($title);
      const $content = AJS.$("<p></p>");
      $content.text(content);
      $message.append($content);
      $messages.append($message);
    }
  };

  const createSearchDialog = function (i18n, title) {
    if (logToConsole) log("Creating dialog '" + title + "' ...");
    const $dialog = AJS.$("<section id='projectdoc-search-dialog-tool' hidden></section>");
    $dialog.addClass("aui-dialog2 aui-dialog2-xlarge aui-layer");
    $dialog.attr("role", "dialog");
    $dialog.attr("tabindex", "-1");
    $dialog.attr("aria-labelledby", "projectdoc-search-dialog-header--heading");

    const $dialogHeader = AJS.$("<header id='projectdoc-search-dialog-header'></header>");
    $dialogHeader.addClass("aui-dialog2-header");
    const $dialogHeaderTitle = AJS.$("<h1 id='projectdoc-search-dialog-header--heading'></h1>");
    $dialogHeaderTitle.addClass("aui-dialog2-header-main");
    $dialogHeaderTitle.text(title);
    $dialogHeader.append($dialogHeaderTitle);
    $dialog.append($dialogHeader);

    const $dialogContent = AJS.$("<div id='projectdoc-search-dialog'></div>");
    $dialogContent.addClass("aui-dialog2-content");
    const $messages = AJS.$("<div id='projectdoc-messages'></div>");
    $dialogContent.append($messages);

    const $searchResults = AJS.$("<div id='projectdoc-search-results'></div>");
    const $searchForm = createSearchForm(i18n, $searchResults);
    $dialogContent.append($searchForm);
    $dialogContent.append($searchResults);
    $dialog.append($dialogContent);

    const $dialogFooter = AJS.$("<footer id='projectdoc-search-dialog-footer'></footer>");
    $dialogFooter.addClass("aui-dialog2-footer");
    const $dialogButtons = AJS.$("<div id='aui-dialog2-footer-actions' class='aui-dialog2-footer-actions'></div>");
    $dialogFooter.append($dialogButtons);
    $dialog.append($dialogFooter);

    const $dialogButtonClose = AJS.$("<button id='projectdoc-search-dialog-close'></button>");
    $dialogButtonClose.attr("title", "Close search tool window ...");
    $dialogButtonClose.addClass("aui-button aui-button-link");
    $dialogButtonClose.text("Close");
    $dialogButtons.append($dialogButtonClose);
    AJS.$($dialogButtonClose).on('click', function (e) {
      e.preventDefault();
      AJS.dialog2("#projectdoc-search-dialog-tool").hide();
    });

    const $dialogHint = AJS.$("<div class='aui-dialog2-footer-hint'></div>");
    $dialogFooter.append($dialogHint);

    const $dialogButtonHelp = AJS.$("<button id='projectdoc-search-dialog-help' class='aui-button toolbar-item' style='margin-right: 10px;'><span class='aui-icon aui-icon-small aui-iconfont-question-filled'>Help</span></button>\n");
    $dialogButtonHelp.attr("title", "Go to search tips on smartics.eu ...");
    $dialogHint.append($dialogButtonHelp);
    AJS.$($dialogButtonHelp).on('click', function (e) {
      e.preventDefault();
      window.open("https://www.smartics.eu/confluence/x/dAVk");
    });
    return $dialog;
  };

  const i18n = PDBMLS.fetchI18n(baseUrl, ["name", "shortDescription"]);
  const $searchDialogWindow = createSearchDialog(i18n, "projectdoc Search Tool");
  // AJS.dialog2($searchDialogWindow).hide();
  // AJS.$('#page').append($searchDialogWindow);

  const openSearchTool = function () {
    if (logToConsole) log("Opening window ...");
    AJS.dialog2($searchDialogWindow /*'#projectdoc-search-dialog-tool'*/).show();
  };

  AJS.whenIType('11').execute(function () {
    openSearchTool();
  });

  if (logToConsole) log("Adding menu ...");
  const menuId = "inspect";
  const sectionId = "projectdoc-inspect-menu-search-section";

  USERSCRIPT4C_MENU.addSection(menuId, {
    id: sectionId,
    label: "Search",
    weight: 100
  });

  USERSCRIPT4C_MENU.addMenuItem(sectionId, {
    id: "projectdoc-menu-inspect-item-search-tool",
    label: "Launch Search Tool",
    weight: "10"
  }, openSearchTool);
});

Details

More information on using this userscript.

Search Tool Dialog

The projectdoc Search Tool is rendered as a dialog within the current page.

The help button currently directs directly to the Search Tips on this server.

Inspect Menu

In case the Inspect Menu for projectdoc is installed, the userscript will be added as a menu item.

Per default the search dialog will open on clicking the shortcut '11'.

Related Scripts

NameShort Description
Hide projectdoc Tools
Removes projectdoc tools (blueprints and macros) from the current page.
Inspect Menu for projectdoc
Renders a menu with tools to inspect information from a projectdoc document, shown in the browser.
Refactor projectdoc Document
Adds a refactor menu and checks the current document for property issues.