Skip to content

Commit

Permalink
Merge pull request #35 from patridge/dev
Browse files Browse the repository at this point in the history
v0.6.0
  • Loading branch information
patridge committed Jun 10, 2020
2 parents 2572811 + 30a0ac5 commit 4b4dd61
Show file tree
Hide file tree
Showing 10 changed files with 90 additions and 130 deletions.
28 changes: 22 additions & 6 deletions README.md
@@ -1,20 +1,27 @@
# Learn metadata maintenance tool
# Learn Maintenance Tool

Determine the author of a given Microsoft Learn or Microsoft Docs page. And quickly navigate to the content in GitHub to propose edits. This tool was created for the Microsoft Learn content team to help triage user-reported feedback to the right maintainer, but anyone is welcome to use it if it helps them.

![Screenshot of the Microsoft Learn maintenance tool Chrome extension showing a page's author, date, and edit link metadata loaded.](media/extension-screenshot-large-v0.2.5.png)

## Features

Extract the critical page metadata fields into a Chrome extension pop-up display, with clickable links to the YAML and Markdown pages for editing directly in GitHub.
### View Microsoft Learn and Microsoft Docs page metadata

When you are viewing a Microsoft Learn or Microsoft Docs content page, clicking the Learn Maintenance Tool extension will show a pop-up with useful page metadata fields, each with a copy button to allow for easy pasting where you need it. The pop-up also includes clickable links to open the YAML and Markdown pages for editing directly in GitHub.

This is the information currently being extracted:

* `ms.author`
* `author`
* `ms.date`
* `uid`
* Edit URL(s), either `original_content_git_url` or a modified version of `original_ref_skeleton_git_url` for YAML and/or Markdown pages

### View Microsoft Learn content page from Azure DevOps customer feedback work items

When you are viewing a customer feedback work item for a Microsoft Learn page the Learn Maintenance Tool extension will show a pop-up with some userful metadata fields and a link to view other open feedback for this unit and parent module as well as any customer feedback rating verbatims. This way, you can tackle several work items in a single maintenance session.

## Installation

If you are using Google Chrome or the Chromium-based Microsoft Edge, you can install the [Microsoft Learn maintenance tool extension](https://chrome.google.com/webstore/detail/microsoft-learn-maintenan/kagphmnlicelfcbbhhmgjcpgnbponlda) to allow retrieving Learn page metadata from the browser toolbar.
Expand Down Expand Up @@ -46,15 +53,24 @@ For Microsoft Edge, you'll first need to allow installing extensions from other
1. Confirm the extension install by clicking the **Add extension** button from the resulting pop-up.
![Screenshot of pop-up prompt confirming Chrome extension install](media/edge-confirm-extension-install.png)

## Release notes

### v0.6.0:

* Allow use on Azure DevOps from alternate domain: dev.azure.com/{team}/{project}
* Offer UID when gathering metadata for Learn content pages
* Fix issues with correct pop-up not loading in some situations
* Truncate long metadata values while offering full text in a hover value
* Fix GitHub URL for Docs pages with the correct branch

## Roadmap

Here are the current plans for upcoming releases. These are definitely subject to change as this project develops or evolves.

### v0.5+: Customization
### v0.7+: Customization

* Allow customizing which metadata fields are important to you

### v???: The Future
### Future plans and suggestions

* Offer interactions with Azure DevOps work items directly
* Offer area path interpretation or intelligent guess for work item categorization
You can follow along with planned development efforts by [looking at the open GitHub issues with the **enhancement** tag](https://github.com/patridge/learn-metadata-tool/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement). If you have a feature or suggestion you want to propose, [submit your own enhancement request on GitHub](https://github.com/patridge/learn-metadata-tool/issues/new?labels=enhancement).
10 changes: 6 additions & 4 deletions azure-devops-extension-popup.html
@@ -1,15 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<style>
</style>
<link rel="stylesheet" href="normalize.min.css" />
<link rel="stylesheet" href="pop-up.css" />
<script defer src="/js/all.min.js"></script>
</head>
<body>
<table style="width: 250px">
<table>
<tr>
<!-- TODO: swap to a field or styling where we can shorten the display of lengthy UIDs. (Then remove width style above.) -->
<td>UID</td><td><a target="_blank" id="contentUrl"><span class="copy-field-target" style="overflow: hidden;" id="uid">...</span></a></td><td><button class="copy-field-btn"><span class="fas fa-clipboard"></span></button></td>
<td class="copy-field-label">UID</td>
<td><a target="_blank" id="contentUrl"><span class="copy-field-target" style="overflow: hidden;" id="uid">...</span></a></td>
<td><button class="copy-field-btn" title="Copy to clipboard"><span class="fas fa-clipboard"></span></button></td>
</tr>
<tr>
<td colspan="2"><a target="_blank" id="relatedWorkItemsQueryUrl">Related issues</a> (<a target="_blank" id="relatedVerbatimsQueryUrl">Verbatims</a>)</td>
Expand Down
6 changes: 4 additions & 2 deletions azure-devops-extension-popup.js
Expand Up @@ -24,10 +24,11 @@ let displayWorkItemData = async function (workItemData) {
*/
const uid = workItemData.UID;
uidSpan.textContent = uid;
uidSpan.title = uid;

// For related items, search for immediate UID and next level up
// e.g., learn.area.module.1-unit -> [ learn.area.module.1-unit, learn.area.module ]
let uidPeriodCount = uid.length - uid.replace(".", "").length;
//let uidPeriodCount = uid.length - uid.replace(".", "").length;
let uidWithoutLastSection = uid.slice(0, uid.lastIndexOf("."));
let uidSubstrings = [uid, uidWithoutLastSection];
let uidQuery = `'${uidSubstrings.join("','")}'`;
Expand Down Expand Up @@ -77,7 +78,8 @@ chrome.tabs.query({ active: true, currentWindow: true },
let tempAnchor = document.createElement("a");
tempAnchor.href = tabs[0].url;
let tabId = tabs[0].id;
if (tempAnchor.hostname.endsWith("visualstudio.com")) {
let host = tempAnchor.hostname;
if (host.endsWith("visualstudio.com") || host === "dev.azure.com") {
chrome.tabs.executeScript(
tabId,
{
Expand Down
36 changes: 22 additions & 14 deletions background.js
Expand Up @@ -13,7 +13,7 @@ let setPopUpByTabId = function (tabId) {
popup: "learn-extension-popup.html"
});
}
else if (host.endsWith("visualstudio.com")) {
else if (host.endsWith("visualstudio.com") || host === "dev.azure.com") {
chrome.browserAction.setPopup({
tabId: tabId,
popup: "azure-devops-extension-popup.html"
Expand Down Expand Up @@ -61,30 +61,38 @@ chrome.runtime.onInstalled.addListener(function() {
// We could make a bunch of nearly identical rules for these or catch more than intended and handle edge cases elsewhere in code. So far, we are choosing the later.
hostSuffix: "visualstudio.com",
},
pageUrl: {
// We are hoping to allow this extension whenever we can. That includes the following URL examples.
// * Azure DevOps (alt location): https://dev.azure.com/
// Not super specific here, but may be good enough (other options: https://developer.chrome.com/extensions/declarativeContent#type-PageStateMatcher).
// We could make a bunch of nearly identical rules for these or catch more than intended and handle edge cases elsewhere in code. So far, we are choosing the later.
hostSuffix: "dev.azure.com",
},
})
],
actions: [
new chrome.declarativeContent.ShowPageAction()
]
// To call other code during actions, wrap up things in an immediately executed function and return the `ShowPageAction` result.
// actions: [(() => {
// // NOTE: These calls may fire immediately after the extension is loaded, not when deciding when to show something on a given tab. (Maybe they only fire when an applicable tab is already open???)
// setPopUpViaActiveTabQuery();
// return new chrome.declarativeContent.ShowPageAction();
// })()]
}
]);

// NOTE: This handles when you switch between tabs to readdress which pop-up is shown.
chrome.tabs.onActivated.addListener(function (activeInfo) {
setPopUpByTabId(activeInfo.tabId);
});
// NOTE: This handles when you navigate between pages _within_ a tab to readdress which pop-up is shown. (Sometimes, if a page didn't need a pop-up and you navigated to one that should have a pop-up, it wasn't being shown.)
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
// This gets called a lot! Restrict to only onUpdated calls where the URL in the tab was changed.
if (changeInfo.url) {
setPopUpByTabId(tabId);
}
});
// TODO: Add a default pop-up that isn't set for a page, in case we haven't run anything yet.
});
});

// NOTE: This handles when you switch between tabs to readdress which pop-up is shown.
chrome.tabs.onActivated.addListener(function (activeInfo) {
setPopUpByTabId(activeInfo.tabId);
});
// NOTE: This handles when you navigate between pages _within_ a tab to readdress which pop-up is shown. (Sometimes, if a page didn't need a pop-up and you navigated to one that should have a pop-up, it wasn't being shown.)
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
// This gets called a lot! Restrict to only onUpdated calls where the URL in the tab was changed.
if (changeInfo.url) {
setPopUpByTabId(tabId);
}
});
// TODO: Add a default pop-up that isn't set for a page, in case we haven't run anything yet.
4 changes: 3 additions & 1 deletion docs-extension-popup.js
@@ -1,4 +1,5 @@
// NOTE: This script executes in the context of the pop-up itself (vs. below, where we execute a script in the target tab).
let uidSpan = document.querySelector("#uid");
let msAuthorSpan = document.getElementById("msAuthor");
let gitHubAuthorSpan = document.getElementById("gitHubAuthor");
let msDateSpan = document.getElementById("msDate");
Expand All @@ -18,6 +19,7 @@ copyButtons.forEach(btn => {
});

let displayMetadata = async function (metadata) {
uidSpan.textContent = metadata.uid;
msAuthorSpan.textContent = metadata.msAuthorMetaTagValue;
gitHubAuthorSpan.textContent = metadata.gitHubAuthorMetaTagValue;
msDateSpan.textContent = metadata.msDateMetaTagValue;
Expand Down Expand Up @@ -54,7 +56,7 @@ chrome.runtime.onMessage.addListener(async function (request, sender, sendRespon
chrome.tabs.query({ active: true, currentWindow: true },
function(tabs) {
// NOTE: This system duplicates a lot of the background.js PageStateMatcher system manually. There is probably a better way.
const microsoftLearnPageScript = "get-author.js";
const microsoftLearnPageScript = "get-docs-metadata.js";
let tempAnchor = document.createElement("a");
tempAnchor.href = tabs[0].url;
let tabId = tabs[0].id;
Expand Down
105 changes: 8 additions & 97 deletions get-author.js → get-docs-metadata.js
@@ -1,70 +1,10 @@
// NOTE: Had to stuff everything in this immediately executing function to avoid duplicate declaration errors when this script was run every time the pop-up was loaded. Probably a better way to handle this, though.
(async function () {
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
let storageLocalGetAsync = function (keys) {
// TODO: Add some sort of expiration logic to set/get.
let gotValue = new Promise((resolve, reject) => {
chrome.storage.local.get(
keys, // NOTE: `null` will get entire contents of storage
function (result) {
// Keys could be a string or an array of strings (or any object to get back an empty result, or null to get all of cache).
// Unify to an array regardless.
let keyList = Array.isArray(keys) ? [...keys] : [keys];
for (var keyIndex in keyList) {
var key = keyList[keyIndex];
if (result[key]) {
console.log({status: `Cache found: [${key}]`, keys, result });
}
else {
console.log({status: `Cache miss: [${key}]`, keys });
}
}
resolve(result);
}
);
});
return gotValue;
};
let storageLocalSetAsync = function (items) {
// TODO: Add some sort of expiration logic to set/get.
let setValue = new Promise((resolve, reject) => {
chrome.storage.local.set(
items,
function () {
// If this cache call fails, Chrome will have set `runtime.lastError`.
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message || `Cache set error: ${chrome.runtime.lastError}`));
}
else {
resolve();
}
}
);
});
return setValue;
};
let storageLocalRemoveAsync = async function (keys) {
let removeValue = new Promise((resolve, reject) => {
chrome.storage.local.remove(
keys,
function () {
// If this cache call fails, Chrome will have set `runtime.lastError`.
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message || `Error retrieving cache: ${chrome.runtime.lastError}`));
}
else {
resolve();
}
}
);
});
};

// storageLocalRemoveAsync([ location ]);
let getCurrentPageMetadata = function () {
let metaTags = document.getElementsByTagName("meta");
let uidTag = [...metaTags].filter(meta => meta.getAttribute("name") === "uid")[0];
let uid = uidTag ? uidTag.getAttribute("content") : "";
let msAuthorTag = [...metaTags].filter(meta => meta.getAttribute("name") === "ms.author")[0];
let msAuthor = msAuthorTag ? msAuthorTag.getAttribute("content") : "";
let authorTag = [...metaTags].filter(meta => meta.getAttribute("name") === "author")[0];
Expand Down Expand Up @@ -92,30 +32,26 @@
let gitUrlTag = [...metaTags].filter(meta => meta.getAttribute("name") === "original_content_git_url")[0];
// e.g., <meta name="original_content_git_url" content="https://github.com/MicrosoftDocs/learn-docs/blob/master/learn-docs/docs/support-triage-issues.md" />
// Use the raw URL for Markdown edit location. (YAML edit location doesn't exist.)

let gitMarkdownEditUrl = gitUrlTag ? gitUrlTag.getAttribute("content") : "";
let gitMarkdownMasterEditUrl = gitMarkdownEditUrl.replace("/live/", "/master/");
let gitYamlEditUrl = null; // ?not applicable outside Learn?
return {
gitYamlEditUrl: null,
gitMarkdownEditUrl
gitYamlEditUrl,
gitMarkdownEditUrl: gitMarkdownMasterEditUrl
};
}
})(metaTags);

return {
uid,
msAuthorMetaTagValue: msAuthor,
gitHubAuthorMetaTagValue: author,
msDateMetaTagValue: msDate,
gitHubYamlLocation: gitUrlValues.gitYamlEditUrl,
gitHubMarkdownLocation: gitUrlValues.gitMarkdownEditUrl,
};
};
let cachePageMetadata = async function (location, pageMetadata) {
// ???pageMetadata = getCurrentPageMetadata();
let cacheAddition = {};
cacheAddition[location] = pageMetadata;
await storageLocalSetAsync(cacheAddition);
};
let sendPopUpUpdateRequest = function (pageMetadata) {
chrome.runtime.sendMessage(
{
Expand All @@ -133,31 +69,6 @@
);
};

let location = document.location.href;

// Try to get cached metadata as a placeholder.
var pageMetadata = await (async function () {
var cachedMetadata = await storageLocalGetAsync([ location ]);
return cachedMetadata[location];
})();
var wasPageMetadataCached = false;

// If no cached data, get current.
if (!pageMetadata) {
pageMetadata = getCurrentPageMetadata();
cachePageMetadata(location, pageMetadata);
}
else {
wasPageMetadataCached = true;
}

var pageMetadata = getCurrentPageMetadata();
sendPopUpUpdateRequest(pageMetadata);

// If we used cached metadata, get the latest and update the cache.
if (wasPageMetadataCached) {
await delay(5000);
pageMetadata = getCurrentPageMetadata();
cachePageMetadata(location, pageMetadata);
sendPopUpUpdateRequest(pageMetadata);
}
})();
7 changes: 5 additions & 2 deletions learn-extension-popup.html
@@ -1,8 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<style>
</style>
<link rel="stylesheet" href="normalize.min.css" />
<link rel="stylesheet" href="pop-up.css" />
<script defer src="/js/all.min.js"></script>
</head>
<body>
Expand All @@ -13,6 +13,9 @@
<tr>
<td>author</td><td><span id="gitHubAuthor" class="copy-field-target">...</span></td><td><button class="copy-field-btn"><span class="fas fa-clipboard"></span></button></td>
</tr>
<tr>
<td>UID</td><td><span id="uid" class="copy-field-target">...</span></td><td><button class="copy-field-btn"><span class="fas fa-clipboard"></span></button></td>
</tr>
<tr>
<td>ms.date</td><td><span id="msDate">...</span></td>
</tr>
Expand Down
5 changes: 1 addition & 4 deletions manifest.json
Expand Up @@ -2,9 +2,8 @@
"name": "Microsoft Learn maintenance tool",
"short_name": "LearnTool",
"description": "Helping you maintain content on Microsoft Learn and Docs. Extract metadata from content pages for editing or triage purposes.",
"version": "0.5.0",
"version": "0.6.0",
"permissions": [
// TODO: Research a match pattern instead of `activeTab` (per note in developer dashboard permission justifcation section): https://developer.chrome.com/extensions/match_patterns
"activeTab",
"tabs",
"declarativeContent",
Expand All @@ -16,8 +15,6 @@
],
"persistent": false
},
// Switched from `page_action` to `browser_action` to get `chrome.browserAction.setPopup` to be defined in code. Previously had everything working fine in `page_action`, but now I have no idea why.
// It feels like I should be able to do page_actions for each domain or something, but haven't figured that out (per docs: https://developer.chrome.com/extensions/browserAction#tips).
"browser_action": {
"default_popup": "learn-extension-popup.html",
"default_icon": {
Expand Down

0 comments on commit 4b4dd61

Please sign in to comment.