adding the code for the first time (#1)

* add the code

* PR changes

* reformat readme
This commit is contained in:
Jay Windsor 2019-05-28 15:56:30 -07:00 коммит произвёл GitHub
Родитель 1e901b8eef
Коммит e44afaeb89
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
35 изменённых файлов: 10911 добавлений и 331 удалений

337
.gitignore поставляемый
Просмотреть файл

@ -1,330 +1,19 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
### No node_modules.
node_modules/
# Visual Studio 6 build log
*.plg
### No built/compiled code.
dist/
# Visual Studio 6 workspace options file
*.opt
### No *.js and *.js.map files.
**/*.js
**/*.js.map
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
### No *.css and *.css.map files.
**/*.css
**/*.css.map
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
### No package files.
**/*.vsix
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
### Exceptions.
!config/webpack/webpack.*.js

7
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
{
"editor.detectIndentation": false,
"editor.insertSpaces": true,
"editor.tabSize": 4,
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}

Просмотреть файл

@ -1,5 +1,29 @@
Wiql to OData translates an Azure DevOps query into an OData query for use with [Azure DevOps Analytics](https://marketplace.visualstudio.com/items?itemName=ms.vss-analytics) [OData endpoints](https://docs.microsoft.com/en-us/azure/devops/report/extend-analytics/?view=vsts).
# Contributing
![](images/readme/screenshot.png)
## Features
- Convert 'flat list of work items' and 'work items and direct links' ADO queries to OData queries with the click of a button. These queries are url formatted and ready to be used in your apps with any http client.
## Restrictions
- You must have [Azure DevOps Analytics](https://marketplace.visualstudio.com/items?itemName=ms.vss-analytics) installed to your account.
- As with the Azure DevOps Analytics extension, *this extension is currently in preview*. It will not support some scenarios that OData or ADO Analytics currently doesn't support. For example...
- Recursive (tree) Wiql queries are not supported.
- Macros (such as `@project`, `@me`), are not supported. When possible, these values will be replaced with static values. `@today` is supported, but mathematical operations on `@today` such as `@today - 1` are not supported, and will be replaced with a static value.
- The output query is a best guess, and may require some adjustment to perform exactly as required. Please heed the warnings listed below the query text.
## Reporting Security Issues
Security issues and bugs should be reported privately, via email, to the Microsoft Security
Response Center (MSRC) at [secure@microsoft.com](mailto:secure@microsoft.com). You should
receive a response within 24 hours. If for some reason you do not, please follow up via
email to ensure we received your original message. Further information, including the
[MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in
the [Security TechCenter](https://technet.microsoft.com/en-us/security/default).
## Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us

Просмотреть файл

@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
export const environment = {
buttonText: 'Translate To OData (Dev)',
};

Просмотреть файл

@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
export const environment = {
buttonText: 'Translate To OData',
};

Просмотреть файл

@ -0,0 +1,41 @@
{
"id": "wiql-to-odata-dev",
"version": "0.0.177",
"publisher": "engoy-dev",
"public": false,
"name": "Wiql to OData (Dev)",
"contributions": [
{
"id": "work-item-query-menu",
"type": "ms.vss-web.action-provider",
"description": "Convert a WIQL query to an OData query.",
"targets": [
"ms.vss-work-web.work-item-query-menu"
],
"properties": {
"group": "contributed",
"uri": "index.html"
}
},
{
"id": "work-item-query-results-toolbar-menu",
"type": "ms.vss-web.action-provider",
"description": "Convert a WIQL query to an OData query.",
"targets": [
"ms.vss-work-web.work-item-query-results-toolbar-menu"
],
"properties": {
"group": "contributed",
"uri": "index.html"
}
},
{
"id": "app-dialog",
"type": "ms.vss-web.control",
"targets": [],
"properties": {
"uri": "dialog.html"
}
}
]
}

Просмотреть файл

@ -0,0 +1,41 @@
{
"id": "wiql-to-odata",
"version": "0.0.1",
"publisher": "ms-eswm",
"public": true,
"name": "Wiql to OData",
"contributions": [
{
"id": "work-item-query-menu",
"type": "ms.vss-web.action-provider",
"description": "Convert a WIQL query to an OData query.",
"targets": [
"ms.vss-work-web.work-item-query-menu"
],
"properties": {
"group": "contributed",
"uri": "index.html"
}
},
{
"id": "work-item-query-results-toolbar-menu",
"type": "ms.vss-web.action-provider",
"description": "Convert a WIQL query to an OData query.",
"targets": [
"ms.vss-work-web.work-item-query-results-toolbar-menu"
],
"properties": {
"group": "contributed",
"uri": "index.html"
}
},
{
"id": "app-dialog",
"type": "ms.vss-web.control",
"targets": [],
"properties": {
"uri": "dialog.html"
}
}
]
}

Просмотреть файл

@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const path = require('path');
const webpack = require('webpack');
const CssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const rootDir = path.resolve(__dirname, '../..');
if (process.env.ENV_FILE == null) {
process.env.ENV_FILE = process.env.NODE_ENV;
}
const extractSass = new CssExtractPlugin({
filename: '[name].[hash].css',
});
const indexHtml = new HtmlWebpackPlugin({
template: path.join(rootDir, './src/index.html'),
filename: path.join(rootDir, './dist/index.html'),
inject: false,
});
const dialogHtml = new HtmlWebpackPlugin({
template: path.join(rootDir, './src/dialog.html'),
filename: path.join(rootDir, './dist/dialog.html'),
inject: false,
});
module.exports = {
target: 'web',
entry: {
app: './src/app/app.ts',
dialog: './src/dialog/dialog.tsx',
polyfills: './src/polyfills.ts'
},
output: {
path: path.join(rootDir, './dist/bundles'),
filename: '[name].[hash].js',
libraryTarget: 'amd'
},
externals: [
/^TFS\/.*/,
/^VSS\/.*/,
],
resolve: {
extensions: [
'.ts',
'.js',
".tsx",
],
alias: {
'@ms/configuration-environment': path.join(rootDir, './config/environment/environment.' + process.env.ENV_FILE),
}
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
}
},
{
enforce: 'pre',
test: /\.js$/,
loader: 'source-map-loader',
exclude: [
/\/node_modules\//
]
},
{
test: /\.scss$|\.sass$/,
use: [
{
loader: CssExtractPlugin.loader,
options: { }
},
"css-loader",
"sass-loader"
],
}
]
},
devtool: 'source-map',
plugins: [
extractSass,
indexHtml,
dialogHtml,
],
}

Просмотреть файл

@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
process.env.NODE_ENV = process.env.NODE_ENV || 'dev';
const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.common');
const DefinePlugin = require('webpack/lib/DefinePlugin');
module.exports = function (env) {
return Merge(CommonConfig, {
mode: 'development',
plugins: [
new DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'dev'),
},
}),
]
});
}

Просмотреть файл

@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Specific environment; can be overriden.
process.env.NODE_ENV = process.env.NODE_ENV || 'prod';
const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.common');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = function (env) {
return Merge(CommonConfig, {
mode: 'production',
plugins: [
new DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'prod'),
}
}),
new UglifyJSPlugin({
sourceMap: false,
}),
],
});
}

Двоичные данные
images/favicon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.4 KiB

Двоичные данные
images/favicon16x16.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичные данные
images/readme/screenshot.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 27 KiB

8749
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

56
package.json Normal file
Просмотреть файл

@ -0,0 +1,56 @@
{
"name": "wiql-to-odata",
"private": true,
"version": "1.0.0",
"scripts": {
"build:dev": "rimraf ./dist && cross-env NODE_ENV=dev webpack --progress --colors --config ./config/webpack/webpack.dev.js",
"package:dev": "npm run package:vsts-extension --vssconfig=dev",
"publish:dev": "npm run publish:vsts-extension --vssconfig=dev",
"deploy:dev": "npm run build:dev && npm run package:dev && npm run publish:dev",
"build:prod": "rimraf ./dist && cross-env NODE_ENV=prod webpack --progress --colors --config ./config/webpack/webpack.prod.js",
"package:prod": "npm run package:vsts-extension --vssconfig=prod",
"publish:prod": "npm run publish:vsts-extension --vssconfig=prod",
"package:vsts-extension": "tfx extension create --manifests ./vss-extension-base.json --overrides-file ./config/vss/vss-extension-%npm_config_vssconfig%.json --rev-version",
"publish:vsts-extension": "tfx extension publish --manifests ./vss-extension-base.json --overrides-file ./config/vss/vss-extension-%npm_config_vssconfig%.json",
"test": "karma start ./config/karma.conf.js",
"lint": "tslint -c tslint.json 'src/**/*.ts"
},
"devDependencies": {
"@types/jasmine": "^2.8.11",
"@types/react": "^16.4.18",
"@types/react-dom": "^16.0.9",
"cross-env": "^5.0.1",
"css-loader": "^1.0.0",
"html-webpack-plugin": "^3.0.0",
"jasmine": "^3.3.0",
"jasmine-core": "^3.0.0",
"karma": "^3.0.0",
"karma-chrome-launcher": "^2.2.0",
"karma-jasmine": "^1.1.0",
"karma-mocha-reporter": "^2.2.3",
"karma-webpack": "^2.0.4",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.10.0",
"rimraf": "^2.6.1",
"sass-loader": "^7.0.0",
"source-map-loader": "^0.2.1",
"ts-loader": "5.3.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"tslint": "5.5.0",
"typescript": "^3.0.0",
"uglifyjs-webpack-plugin": "^2.0.1",
"webpack": "^4.0.0",
"webpack-cli": "^3.1.2",
"webpack-merge": "^4.1.0",
"tslint-eslint-rules": "^5.4.0"
},
"dependencies": {
"core-js": "^2.5.7",
"moment": "2.18.1",
"office-ui-fabric-react": "^6.101.0",
"react": "^16.6.1",
"react-dom": "^16.6.1",
"vss-web-extension-sdk": "5.141.0",
"whatwg-fetch": "^3.0.0"
}
}

84
src/app/app.ts Normal file
Просмотреть файл

@ -0,0 +1,84 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
import { environment } from '@ms/configuration-environment';
import { IActionContext } from '../models';
export const openQueryAction = {
getMenuItems: (context: IActionContext): IContributedMenuItem[] => {
if (!context || !context.query || !context.query.wiql) {
return null;
}
return [{
title: environment.buttonText,
text: environment.buttonText,
icon: 'images/favicon16x16.png',
action: (actionContext: IActionContext) => {
if (actionContext != null && actionContext.query != null && actionContext.query.id != null) {
openDialog(actionContext);
}
},
}];
},
};
export const openQueryOnToolbarAction = {
getMenuItems: (context: IActionContext): IContributedMenuItem[] => {
return [{
title: environment.buttonText,
text: environment.buttonText,
icon: 'images/favicon16x16.png',
action: async (actionContext: IActionContext) => {
if (actionContext && actionContext.query && actionContext.query.wiql) {
openDialog(actionContext);
} else {
const hostDialogService = await VSS.getService<IHostDialogService>(VSS.ServiceIds.Dialog);
hostDialogService.openMessageDialog(
`In order to open your query, please save it first in "My Queries" or "Shared Queries".`,
{
title: 'Unable to perform this operation',
buttons: [hostDialogService.buttons.ok],
});
}
},
}];
},
};
async function openDialog(actionContext: IActionContext) {
const hostDialogService = await VSS.getService<IHostDialogService>(VSS.ServiceIds.Dialog);
const dialog = await hostDialogService.openDialog(
`${extensionContext.publisherId}.${extensionContext.extensionId}.app-dialog`,
{
title: `Translate to OData`,
width: 500,
height: 600,
modal: true,
draggable: true,
resizable: true,
buttons: {
ok: {
id: 'ok',
text: 'Dismiss',
click: () => {
dialog.close();
},
class: 'cta',
},
},
},
actionContext
);
}
const extensionContext = VSS.getExtensionContext();
VSS.register(
`${extensionContext.publisherId}.${extensionContext.extensionId}.work-item-query-menu`,
openQueryAction
);
VSS.register(
`${extensionContext.publisherId}.${extensionContext.extensionId}.work-item-query-results-toolbar-menu`,
openQueryOnToolbarAction
);

33
src/dialog.html Normal file
Просмотреть файл

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<script src="./lib/VSS.SDK.min.js"></script>
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.files.chunks.dialog.css[0] %>" />
<script type="text/javascript">
// Initialize VSTS.
VSS.init({
usePlatformScripts: true,
usePlatformStyles: true,
explicitNotifyLoaded: true,
});
VSS.ready(function () {
VSS.require('<%= htmlWebpackPlugin.files.chunks.polyfills.entry %>', function () {
VSS.require('<%= htmlWebpackPlugin.files.chunks.dialog.entry %>', function (extension) {
// Loading succeeded.
VSS.notifyLoadSucceeded();
});
});
});
</script>
<div id="react-mount"></div>
</body>
</html>

129
src/dialog/dialog-helper.ts Normal file
Просмотреть файл

@ -0,0 +1,129 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
import { QueryExpand, QueryHierarchyItem } from 'TFS/WorkItemTracking/Contracts';
import * as WorkItemTrackingClient from 'TFS/WorkItemTracking/RestClient';
import { NotesService, parseQueryJson, stringToXML } from '../helpers';
import { IActionContext, ODataMetadataParser } from '../models';
import { ODataParser } from '../parsers';
export class DialogHelper {
private actionContext: IActionContext;
private webContext: WebContext;
constructor(context: IActionContext) {
this.actionContext = context;
this.webContext = VSS.getWebContext();
}
/**
* Main Entry point.
* @returns The full url query string.
*/
public async getODataUrl(): Promise<string> {
let query: QueryHierarchyItem;
let metadata: any;
const queryErrorString = 'The query could not be found. Please make sure this is a valid query.';
const axErrorString = 'Could not get ADO Analytics metadata for this account. Please make sure ADO Analytics is enabled.';
NotesService.instance.clearAllNotes();
// Validate
try {
query = await this.getQueryTemp();
} catch (ex) {
NotesService.instance.newNote('error', queryErrorString);
return null;
}
try {
metadata = await this.getODataMetadata();
} catch (ex) {
NotesService.instance.newNote('error', axErrorString);
return null;
}
if (!(query != null && query.wiql != null)) {
NotesService.instance.newNote('error', queryErrorString);
return null;
}
if (metadata == null) {
NotesService.instance.newNote('error', axErrorString);
return null;
}
const parser = new ODataParser(query, metadata);
if (!parser.isQueryValid()) {
return null;
}
// Try to parse and return query.
try {
const allStatements = parser.makeAllStatements();
// Test length.
if (encodeURI(allStatements).length > 2083) {
NotesService.instance.newNote('warning', `The encoded url has ${encodeURI(allStatements).length} characters and will be too long for some browsers and HTTP clients.`);
}
return allStatements;
} catch (ex) {
NotesService.instance.newNote('error', `An unexpected error occured: ${ex.message}`);
return null;
}
}
private async getQuery(): Promise<QueryHierarchyItem> {
const workItemTrackingClient = WorkItemTrackingClient.getClient();
const query = await workItemTrackingClient.getQuery(
this.webContext.project.name,
this.actionContext.query.id,
QueryExpand.All
);
return query;
}
private async getQueryTemp(): Promise<QueryHierarchyItem> {
const accessToken = await VSS.getAccessToken();
const url = `${this.webContext.account.uri}/${this.webContext.project.name}/_apis/wit/queries/${this.actionContext.query.id}?%24expand=3`;
const response = await window.fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken.token}`,
},
});
if (response.status < 200 || response.status >= 400) {
return Promise.reject();
}
const responseText = await response.text();
const responseObj = parseQueryJson(responseText);
return responseObj;
}
private async getODataMetadata(): Promise<any> {
const accessToken = await VSS.getAccessToken();
const url = `https://analytics.dev.azure.com/${this.webContext.account.name}/${this.webContext.project.name}/_odata/v1.0/$metadata`;
const response = await window.fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken.token}`,
},
});
if (response.status < 200 || response.status >= 400) {
return Promise.reject();
}
const responseString = await response.text();
const responseXml = stringToXML(await responseString);
return ODataMetadataParser.createPropertyMap(ODataMetadataParser.parseDocument(responseXml));
}
}

35
src/dialog/dialog.scss Normal file
Просмотреть файл

@ -0,0 +1,35 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
body {
overflow-x: hidden;
overflow-y: auto;
}
.query-name-heading {
margin-top: 0;
}
.note-area {
margin-top: 10px;
max-height: 400px;
overflow: auto;
.note-message-bar {
margin-top: 8px;
}
}
.button-area {
margin: 8px -4px;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-content: stretch;
>.button {
margin: 0 4px;
width: 50%;
}
}

119
src/dialog/dialog.tsx Normal file
Просмотреть файл

@ -0,0 +1,119 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { initializeIcons } from '@uifabric/icons';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { ITextField, TextField } from 'office-ui-fabric-react/lib/TextField';
import { INote, NotesService } from '../helpers/notes-service';
import { IActionContext } from '../models';
import { DialogHelper } from './dialog-helper';
import './dialog.scss';
export interface IAppComponentState {
notes: INote[];
oDataString: string;
loading: boolean;
copied: boolean;
}
class AppComponent extends React.Component<{}, IAppComponentState> {
public textField: ITextField;
public queryName: string;
public constructor(props: any) {
super(props);
initializeIcons();
this.state = {
notes: [],
oDataString: null,
loading: true,
copied: false,
};
}
public async componentDidMount(): Promise<void> {
const config: IActionContext = VSS.getConfiguration();
this.queryName = config.query.name;
const dialogHelper = new DialogHelper(config);
const oDataUrl = await dialogHelper.getODataUrl();
this.setState({
notes: NotesService.instance.notes,
oDataString: oDataUrl,
loading: false,
});
}
public render() {
if (this.state.loading) {
return <Spinner size={SpinnerSize.large} />;
}
return (
<div>
<h2 className='query-name-heading'>{this.queryName}</h2>
{!this.state.notes.map((n) => n.level).includes('error') &&
<div className='query-area'>
<p>Caution: this is not guaranteed to work. Please heed any warnings below and test before use!</p>
<TextField
id='copyField'
multiline rows={8}
spellCheck={false}
defaultValue={this.state.oDataString}
componentRef={(textField) => { this.textField = textField; }}
/>
<div className='button-area'>
<PrimaryButton text={this.state.copied ? 'Copied.' : 'Copy query'}
className='button'
iconProps={{ iconName: this.state.copied ? 'CheckMark' : 'Copy' }}
onClick={() => this.copyClicked()}
/>
<PrimaryButton text='Open in new tab'
className='button'
iconProps={{ iconName: 'OpenInNewWindow' }}
onClick={() => this.openClicked()}
/>
</div>
</div>}
<div className='note-area'>
{this.state.notes.map((note, index) => {
return <MessageBar
key={index}
className='note-message-bar'
messageBarType={MessageBarType[note.level]}
isMultiline={false}
truncated={true}
overflowButtonAriaLabel='See more'
>
{note.message}
</MessageBar>;
})}
</div>
</div>
);
}
private copyClicked() {
this.textField.select();
document.execCommand('copy');
this.setState({ copied: true });
setTimeout(() => this.setState({ copied: false }), 3000);
}
private openClicked() {
window.open(this.state.oDataString, '_blank');
}
}
ReactDOM.render(
<AppComponent />,
document.getElementById('react-mount')
);

7
src/helpers/index.ts Normal file
Просмотреть файл

@ -0,0 +1,7 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
export * from './notes-service';
export * from './query-json-parser';
export * from './xml-parser';

Просмотреть файл

@ -0,0 +1,49 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
export interface INote {
level: 'info' | 'warning' | 'error';
message: string;
}
export class NotesService {
private static instanceInternal: NotesService;
private notesList: INote[] = [];
/**
* @returns the instance (current or new) of NotesService.
*/
public static get instance(): NotesService {
if (this.instanceInternal == null) {
this.instanceInternal = new NotesService();
}
return this.instanceInternal;
}
/**
* @returns gets all deduped notes
*/
public get notes(): INote[] {
// Return unique notes.
return this.notesList.filter((obj, pos, arr) => {
return arr.map((note) => note.message).indexOf(obj.message) === pos;
});
}
/**
* Clears all notes
*/
public clearAllNotes() {
this.notesList = [];
}
/**
* Adds a new note
* @param level info and warning shows the query in the UI. If there are any errors, the query is not show in the UI.
* @param message the message string to show in the UI.
*/
public newNote(level: 'info' | 'warning' | 'error', message: string) {
this.notesList.push({ level, message });
}
}

Просмотреть файл

@ -0,0 +1,44 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
import { LinkQueryMode, LogicalOperation, QueryHierarchyItem, QueryType } from 'TFS/WorkItemTracking/Contracts';
export function parseQueryJson(queryJson: string): QueryHierarchyItem {
return JSON.parse(queryJson, (key: string, value: string) => {
let newValue: any = value;
if (key.toLocaleLowerCase().includes('date')) {
newValue = new Date(value);
} else if (key === 'logicalOperator') {
newValue = LogicalOperation[value.toLocaleUpperCase()];
} else {
switch (value) {
case 'flat':
newValue = QueryType.Flat;
break;
case 'oneHop':
newValue = QueryType.OneHop;
break;
case 'tree':
newValue = QueryType.Tree;
break;
case 'linksOneHopMustContain':
newValue = LinkQueryMode.LinksOneHopMustContain;
break;
case 'linksOneHopMayContain':
newValue = LinkQueryMode.LinksOneHopMayContain;
break;
case 'linksOneHopDoesNotContain':
newValue = LinkQueryMode.LinksOneHopDoesNotContain;
break;
// We dont really care about any other value, since it's not supported.
default:
newValue = value;
break;
}
}
return newValue;
});
}

Просмотреть файл

@ -0,0 +1,7 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
export function stringToXML(oString) {
return (new DOMParser()).parseFromString(oString, 'text/xml');
}

33
src/index.html Normal file
Просмотреть файл

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<script src="./lib/VSS.SDK.min.js"></script>
<link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.files.chunks.app.css[0] %>" />
<script type="text/javascript">
// Initialize VSTS.
VSS.init({
usePlatformScripts: true,
usePlatformStyles: true,
explicitNotifyLoaded: true,
});
VSS.ready(function () {
VSS.require('<%= htmlWebpackPlugin.files.chunks.app.entry %>', function (extension) {
// Loading succeeded.
VSS.notifyLoadSucceeded();
});
});
</script>
<div id="content">
Loading...
</div>
</body>
</html>

29
src/models/context.ts Normal file
Просмотреть файл

@ -0,0 +1,29 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
/**
* Information about the query, included with the action context (IActionContext)
*/
export interface IQueryObject {
id: string;
isPublic: boolean;
name: string;
path: string;
wiql: string;
}
/**
* Incoming context sent when VSS calls getMenuItems (in app.ts)
*/
export interface IActionContext {
id?: number;
// From work item form
workItemId?: number;
query?: IQueryObject;
queryText?: string;
ids?: number[];
// From backlog/iteration (context menu) and query results (toolbar and context menu)
workItemIds?: number[];
columns?: string[];
}

6
src/models/index.ts Normal file
Просмотреть файл

@ -0,0 +1,6 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
export * from './odata-metadata';
export * from './context';

Просмотреть файл

@ -0,0 +1,260 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
export interface IEntityType {
name: string;
key?: IPropertyRef[];
properties?: IProperty[];
}
export interface IPropertyRef {
name: string;
}
export interface IProperty {
name: string;
type: string;
nullable?: boolean;
annotations?: IAnnotation[];
}
export interface IReferentialConstraint {
property: string;
referencedProperty: string;
}
export interface IEntityContainer {
name: string;
entitySets?: IEntitySet[];
}
export interface IEntitySet {
name: string;
entityType: string;
navigationPropertyBindings?: INavigationPropertyBinding[];
annotations?: IAnnotation[];
}
export interface INavigationPropertyBinding {
path: string;
target: string;
}
export interface IAnnotation {
term: string;
value: any;
}
export interface ISchema {
namespace: string;
entityTypes?: IEntityType[];
entityContainers?: IEntityContainer[];
}
export interface IMetadata {
schemas?: ISchema[];
}
export interface IDefaultField {
fieldType: string;
defaultFieldName: string;
defaultFieldType: string;
}
/**
* Properties that for some reason do not have a ReferenceName. These are added to the property list when parsing.
* Some, such as Iteration and Area, are premapped to primative field,
* while others are mapped to a complex type that must be used in conjunction with defaultFields
*/
export const specialProperties: IProperty[] = [
{ name: 'Project', type: 'Microsoft.VisualStudio.Services.Analytics.Model.Project', annotations: [{ term: 'Ref.ReferenceName', value: 'System.TeamProject' }] },
{ name: 'Iteration/IterationId', type: 'Edm.Guid', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationId' }] },
{ name: 'Iteration/IterationPath', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationPath' }] },
{ name: 'Area/AreaId', type: 'Edm.Guid', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaId' }] },
{ name: 'Area/AreaPath', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaPath' }] },
// Below only used in Links expand, so don't need to include 'Links/'...
{ name: 'LinkTypeReferenceName', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.Links.LinkType' }] },
{ name: 'ChangedOn', type: 'Microsoft.VisualStudio.Services.Analytics.Model.CalendarDate', annotations: [{ term: 'Ref.ReferenceName', value: 'System.ChangedDate' }] },
{ name: 'ClosedOn', type: 'Microsoft.VisualStudio.Services.Analytics.Model.CalendarDate', annotations: [{ term: 'Ref.ReferenceName', value: 'Microsoft.VSTS.Common.ClosedDate' }] },
{ name: 'CreatedOn', type: 'Microsoft.VisualStudio.Services.Analytics.Model.CalendarDate', annotations: [{ term: 'Ref.ReferenceName', value: 'System.CreatedDate' }] },
{ name: 'ResolvedOn', type: 'Microsoft.VisualStudio.Services.Analytics.Model.CalendarDate', annotations: [{ term: 'Ref.ReferenceName', value: 'Microsoft.VSTS.Common.ResolvedDate' }] },
{ name: 'StateChangeOn', type: 'Microsoft.VisualStudio.Services.Analytics.Model.CalendarDate', annotations: [{ term: 'Ref.ReferenceName', value: 'Microsoft.VSTS.Common.StateChangeDate' }] },
{ name: 'Iteration/IterationLevel1', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel1' }] },
{ name: 'Iteration/IterationLevel2', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel2' }] },
{ name: 'Iteration/IterationLevel3', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel3' }] },
{ name: 'Iteration/IterationLevel4', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel4' }] },
{ name: 'Iteration/IterationLevel5', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel5' }] },
{ name: 'Iteration/IterationLevel6', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel6' }] },
{ name: 'Iteration/IterationLevel7', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel7' }] },
{ name: 'Iteration/IterationLevel8', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel8' }] },
{ name: 'Iteration/IterationLevel9', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel9' }] },
{ name: 'Iteration/IterationLevel10', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel10' }] },
{ name: 'Iteration/IterationLevel11', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel11' }] },
{ name: 'Iteration/IterationLevel12', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel12' }] },
{ name: 'Iteration/IterationLevel13', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel13' }] },
{ name: 'Iteration/IterationLevel14', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.IterationLevel14' }] },
{ name: 'Area/AreaLevel1', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel1' }] },
{ name: 'Area/AreaLevel2', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel2' }] },
{ name: 'Area/AreaLevel3', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel3' }] },
{ name: 'Area/AreaLevel4', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel4' }] },
{ name: 'Area/AreaLevel5', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel5' }] },
{ name: 'Area/AreaLevel6', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel6' }] },
{ name: 'Area/AreaLevel7', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel7' }] },
{ name: 'Area/AreaLevel8', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel8' }] },
{ name: 'Area/AreaLevel9', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel9' }] },
{ name: 'Area/AreaLevel10', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel10' }] },
{ name: 'Area/AreaLevel11', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel11' }] },
{ name: 'Area/AreaLevel12', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel12' }] },
{ name: 'Area/AreaLevel13', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel13' }] },
{ name: 'Area/AreaLevel14', type: 'Edm.String', annotations: [{ term: 'Ref.ReferenceName', value: 'System.AreaLevel14' }] },
];
/**
* A list of complex types that can be simplified to primitive types given a default property.
*/
export const defaultFields: IDefaultField[] = [
{ fieldType: 'Microsoft.VisualStudio.Services.Analytics.Model.CalendarDate', defaultFieldName: 'Date', defaultFieldType: 'Edm.DateTimeOffset' },
{ fieldType: 'Microsoft.VisualStudio.Services.Analytics.Model.Project', defaultFieldName: 'ProjectName', defaultFieldType: 'Edm.String' },
{ fieldType: 'Microsoft.VisualStudio.Services.Analytics.Model.User', defaultFieldName: 'UserEmail', defaultFieldType: 'Edm.String' },
];
/**
* Manages the metadata xml containing all OData fields and types.
*/
export class ODataMetadataParser {
private static defaultFieldsInternal = null;
/**
* Parses the $metadata xml into an object.
* @param document The xml returned from the odata $metadata endpoint
*/
public static parseDocument(document: XMLDocument): IMetadata {
const edmx = document.getElementsByTagName('edmx:Edmx')[0];
return {
schemas: this.parseCollection(edmx, 'Schema', (e) => this.parseSchema(e)),
};
}
/**
* @returns A dictionary mapping Ref.ReferenceNames to IProperties.
* @param metadata IMetadata to convert to a dictionary.
*/
public static createPropertyMap(metadata: IMetadata): { [id: string]: IProperty; } {
return metadata.schemas
.reduce<IEntityType[]>(
(acc, schema) => (schema.entityTypes ? acc.concat(schema.entityTypes) : acc) as IEntityType[],
new Array<IEntityType>()
)
.filter((entityType) => {
return entityType.name === 'WorkItem' || entityType.name === 'CustomWorkItem';
})
.reduce<IProperty[]>(
(acc, x) => (x.properties ? acc.concat(x.properties) : acc) as IProperty[],
new Array<IProperty>()
)
.map((p) => {
const referenceName = p.annotations
? p.annotations
.filter((a) => a.term === 'Ref.ReferenceName')
.map((a) => a.value as string)[0]
: undefined;
return { referenceName, property: p };
})
.filter((x) => {
return x.referenceName !== undefined;
})
.reduce<{ [id: string]: IProperty; }>(
(acc, x) => { acc[x.referenceName] = x.property; return acc; },
{}
);
}
/**
* @returns a mapping of OData type names to objects defining the field that should be used to simplify them to primative types.
*/
public static get defaultFields(): { [name: string]: IDefaultField; } {
if (ODataMetadataParser.defaultFieldsInternal == null) {
ODataMetadataParser.defaultFieldsInternal = defaultFields.reduce<{ [name: string]: IDefaultField; }>(
(acc, x) => { acc[x.fieldType] = x; return acc; },
{}
);
}
return ODataMetadataParser.defaultFieldsInternal;
}
//#region "Xml Parsers"
private static parseSchema(element: Element): ISchema {
return {
namespace: element.getAttribute('Namespace'),
entityTypes: this.parseCollection<IEntityType>(element, 'EntityType', (e) => this.parseEntityType(e)),
entityContainers: this.parseCollection<IEntityContainer>(element, 'EntityContainer', (e) => this.parseEntityContainer(e)),
};
}
private static parseEntityContainer(element: Element): IEntityContainer {
return {
name: element.getAttribute('Name'),
entitySets: this.parseCollection<IEntitySet>(element, 'EntitySet', (e) => this.parseEntitySet(e)),
};
}
private static parseEntitySet(element: Element): IEntitySet {
return {
name: element.getAttribute('Name'),
entityType: element.getAttribute('EntityType'),
navigationPropertyBindings: this.parseCollection<INavigationPropertyBinding>(
element,
'NavigationPropertyBinding',
(e) => this.parseNavigationPropertyBinding(e)
),
annotations: this.parseCollection(element, 'Annotation', (e) => this.parseAnnotation(e)),
};
}
private static parseNavigationPropertyBinding(element: Element): INavigationPropertyBinding {
return {
path: element.getAttribute('Path'),
target: element.getAttribute('Target'),
};
}
private static parseEntityType(element: Element): IEntityType {
return {
name: element.getAttribute('Name'),
properties: [
...this.parseCollection<IProperty>(element, 'Property', (e) => this.parseProperty(e)),
...this.parseCollection<IProperty>(element, 'NavigationProperty', (e) => this.parseProperty(e)),
...specialProperties,
],
};
}
private static parseProperty(element: Element): IProperty {
return {
name: element.getAttribute('Name'),
type: element.getAttribute('Type'),
nullable: element.hasAttribute('Nullable') ? !!element.getAttribute('Nullable') : undefined,
annotations: this.parseCollection(element, 'Annotation', (e) => this.parseAnnotation(e)),
};
}
private static parseAnnotation(element: Element): IAnnotation {
// TODO: support different types of annotations
return {
term: element.getAttribute('Term'),
value: element.getAttribute('String'),
};
}
private static parseCollection<T>(element: Element, name: string, select: (element: Element) => T) {
const nodes = element.getElementsByTagName(name);
const result = Array.from(nodes).map((node) => select(node));
return result.length > 0 ? result : undefined;
}
//#endregion "Xml Parsers"
}

Просмотреть файл

@ -0,0 +1,356 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
import { WorkItemFieldReference, WorkItemQueryClause } from 'TFS/WorkItemTracking/Contracts';
import { NotesService } from '../helpers';
import { IProperty, ODataMetadataParser } from '../models';
interface ISupportedOperationsHandling {
name: string;
handler: (clause: WorkItemQueryClause) => string;
}
interface ISupportedTypesHandling {
name: string;
mathAllowed: boolean;
handler: (value: string) => string;
}
interface ILhsRhs {
left: string;
right: string;
}
/**
* Handles the parsing of a single clause into an OData clause.
*/
export class ClauseParser {
/**
* A cache for the storage of the supportedOperationsHandling variable in a map with key = WIQL SupportedOperation name.
* Managed by the supportedOperationsHandlingMap function.
*/
private supportedOperationsHandlingMapInternal: { [id: string]: ISupportedOperationsHandling; };
/**
* A cache for the storage of the supportedTypesHandling variable in a map with key = the OData type name.
* Managed by the supportedTypesHandlingMap function.
*/
private supportedTypesHandlingMapInternal: { [id: string]: ISupportedTypesHandling; };
/**
* A cache for the storage the list of mathAllowed properties from the supportedTypesHandling variable.
* Managed by the _mathAllowedTypes function.
*/
private mathAllowedTypesInternal: string[];
/**
* A mapping of Ref.ReferenceNames to IProperties from IMetadata.
*/
private metadata: { [id: string]: IProperty; };
/**
* VSS Web Context.
*/
private webContext = VSS.getWebContext();
/**
* The WIQL operations and the functions to handle the parsing of the clause containing the operation.
*/
private supportedOperationsHandling: ISupportedOperationsHandling[] = [
{ name: 'SupportedOperations.Contains', handler: (clause) => this.parseContains(clause) },
{ name: 'SupportedOperations.ContainsWords', handler: (clause) => this.parseContainsWord(clause) },
{ name: 'SupportedOperations.NotContainsWords', handler: (clause) => this.parseContainsWord(clause) },
{ name: 'SupportedOperations.Equals', handler: (clause) => this.parseSimpleOperator(clause, 'eq') },
{ name: 'SupportedOperations.EqualsField', handler: (clause) => this.parseSimpleOperator(clause, 'eq') },
{ name: 'SupportedOperations.Ever', handler: (clause) => this.parseEver(clause) },
{ name: 'SupportedOperations.GreaterThan', handler: (clause) => this.parseSimpleOperator(clause, 'gt') },
{ name: 'SupportedOperations.GreaterThanEquals', handler: (clause) => this.parseSimpleOperator(clause, 'ge') },
{ name: 'SupportedOperations.GreaterThanEqualsField', handler: (clause) => this.parseSimpleOperator(clause, 'ge') },
{ name: 'SupportedOperations.GreaterThanField', handler: (clause) => this.parseSimpleOperator(clause, 'gt') },
{ name: 'SupportedOperations.In', handler: (clause) => this.parseIn(clause) },
// { name: 'SupportedOperations.InGroup', handler: () => {} }, // Not Supported by OData.
{ name: 'SupportedOperations.LessThan', handler: (clause) => this.parseSimpleOperator(clause, 'lt') },
{ name: 'SupportedOperations.LessThanEquals', handler: (clause) => this.parseSimpleOperator(clause, 'le') },
{ name: 'SupportedOperations.LessThanEqualsField', handler: (clause) => this.parseSimpleOperator(clause, 'le') },
{ name: 'SupportedOperations.LessThanField', handler: (clause) => this.parseSimpleOperator(clause, 'lt') },
{ name: 'SupportedOperations.NotContains', handler: (clause) => `not ${this.parseContains(clause)}` },
{ name: 'SupportedOperations.NotEquals', handler: (clause) => this.parseSimpleOperator(clause, 'ne') },
{ name: 'SupportedOperations.NotEqualsField', handler: (clause) => this.parseSimpleOperator(clause, 'ne') },
{ name: 'SupportedOperations.NotIn', handler: (clause) => `not ${this.parseIn(clause)}` },
// { name: 'SupportedOperations.NotInGroup', handler: () => {} }, // Not Supported by OData.
{ name: 'SupportedOperations.NotUnder', handler: (clause) => `not ${this.parseUnder(clause)}` },
{ name: 'SupportedOperations.Under', handler: (clause) => this.parseUnder(clause) },
];
/**
* The metadata for each OData type and the functions to validate and parse the WIQL values into OData values.
*/
private supportedTypesHandling: ISupportedTypesHandling[] = [
{ name: 'Edm.Binary', mathAllowed: false, handler: (value) => this.parseWithQuotesAndPrefix(value, 'X') },
{ name: 'Edm.Boolean', mathAllowed: false, handler: (value) => this.validateAndThen(/^true|false$/, value, () => this.parseWithoutQuotes(value)) },
{ name: 'Edm.Byte', mathAllowed: true, handler: (value) => this.parseWithoutQuotes(value) },
{ name: 'Edm.DateTime', mathAllowed: false, handler: (value) => this.parseDateTime(value) },
{ name: 'Edm.Decimal', mathAllowed: true, handler: (value) => this.validateAndThen(/^-?[0-9]+.[0-9]+$/, value, () => this.parseWithoutQuotesAndSuffix(value, 'M')) },
{ name: 'Edm.Double', mathAllowed: true, handler: (value) => this.validateAndThen(/^-?[0-9]+.[0-9]+$/, value, () => this.parseWithoutQuotesAndSuffix(value, 'd')) },
{ name: 'Edm.Single', mathAllowed: true, handler: (value) => this.validateAndThen(/^-?[0-9]+.[0-9]+$/, value, () => this.parseWithoutQuotesAndSuffix(value, 'f')) },
{
name: 'Edm.Guid',
mathAllowed: false,
handler: (value) => this.validateAndThen(/^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$^-?[0-9]+.[0-9]$/, value, () => this.parseWithQuotesAndPrefix(value, 'guid')),
},
{ name: 'Edm.Int16', mathAllowed: true, handler: (value) => this.validateAndThen(/^-?[0-9]+$/, value, () => this.parseWithoutQuotes(value)) },
{ name: 'Edm.Int32', mathAllowed: true, handler: (value) => this.validateAndThen(/^-?[0-9]+$/, value, () => this.parseWithoutQuotes(value)) },
{ name: 'Edm.Int64', mathAllowed: true, handler: (value) => this.validateAndThen(/^-?[0-9]+$/, value, () => this.parseWithoutQuotesAndSuffix(value, 'L')) },
{ name: 'Edm.String', mathAllowed: false, handler: (value) => this.parseWithQuotesAndPrefix(value, '') },
{ name: 'Edm.DateTimeOffset', mathAllowed: false, handler: (value) => this.parseDateTimeOffset(value) },
{ name: 'Microsoft.VisualStudio.Services.Analytics.Model.CalendarDate', mathAllowed: false, handler: (value) => this.parseDateTimeOffset(value) },
{ name: 'Microsoft.VisualStudio.Services.Analytics.Model.Project', mathAllowed: false, handler: (value) => this.parseWithQuotesAndPrefix(value, '') },
{ name: 'Microsoft.VisualStudio.Services.Analytics.Model.User', mathAllowed: false, handler: (value) => this.parseEmailUser(value) },
];
public constructor(metadata: { [id: string]: IProperty; }) {
this.metadata = metadata;
}
/**
* The entry for the clause parser.
* @param clause the individual clause to be parsed to an OData string
* @returns The OData clause if it can be parsed, else null. In the case of null, notes are added to the NotesService as to why.
*/
public parseClause(clause: WorkItemQueryClause): string {
const handling = this.supportedOperationsHandlingMap[clause.operator.referenceName];
if (handling == null) {
// Verify operator.
NotesService.instance.newNote('warning', `The operation '${clause.operator.name}' is not supported by OData. Leaving the clause out of $filter.`);
return null;
}
// Verify fields. There is always a field on the left hand side, and sometimes on the right hand side.
const lhsMetadata = this.metadata[clause.field.referenceName];
const rhsMetadata = clause.isFieldValue ? this.metadata[clause.fieldValue.referenceName] : this.metadata[clause.field.referenceName];
if (lhsMetadata == null || (clause.isFieldValue && rhsMetadata)) {
NotesService.instance.newNote(
'warning',
`There is no OData equivalent property for field ${lhsMetadata == null ? clause.field.referenceName : clause.fieldValue.referenceName}. Leaving the clause out of $filter.`
);
return null;
}
try {
return handling.handler(clause);
} catch (e) {
NotesService.instance.newNote('warning', `There was a problem parsing the clause for field ${clause.field.referenceName}: ${e.message}. Leaving the clause out of $filter.`);
return null;
}
}
/**
* For the storage of the supportedOperationsHandling variable in a map with key = WIQL SupportedOperation name.
*/
private get supportedOperationsHandlingMap(): { [id: string]: ISupportedOperationsHandling; } {
if (this.supportedOperationsHandlingMapInternal == null) {
this.supportedOperationsHandlingMapInternal = this.supportedOperationsHandling.reduce<{ [name: string]: ISupportedOperationsHandling; }>(
(acc, x) => { acc[x.name] = x; return acc; },
{}
);
}
return this.supportedOperationsHandlingMapInternal;
}
/**
* For the storage of the supportedTypesHandling variable in a map with key = the OData type name.
*/
private get supportedTypesHandlingMap(): { [id: string]: ISupportedTypesHandling; } {
if (this.supportedTypesHandlingMapInternal == null) {
this.supportedTypesHandlingMapInternal = this.supportedTypesHandling.reduce<{ [name: string]: ISupportedTypesHandling; }>(
(acc, x) => { acc[x.name] = x; return acc; },
{}
);
}
return this.supportedTypesHandlingMapInternal;
}
/**
* For the storage the list of mathAllowed properties from the supportedTypesHandling variable.
*/
private get mathAllowedTypes(): string[] {
if (this.mathAllowedTypesInternal == null) {
this.mathAllowedTypesInternal = this.supportedTypesHandling.filter((h) => h.mathAllowed === true).map((h) => h.name);
}
return this.mathAllowedTypesInternal;
}
/**
* Gets the left hand side and the right hand side of the OData clause, converting it from WIQL field names and values.
*/
private getLhsRhs(clause: WorkItemQueryClause): ILhsRhs {
const lhs = this.odataExpressionFromField(clause.field);
const rhs = clause.isFieldValue ? this.odataExpressionFromField(clause.fieldValue) : this.odataExpressionFromValue(clause.field, clause.value);
return { right: rhs, left: lhs };
}
/**
* @returns the string name of the OData property corresponding to the WIQL field.
* @param field The field reference of the field to convert to an OData property.
*/
private odataExpressionFromField(field: WorkItemFieldReference): string {
const odataProperty = this.metadata[field.referenceName];
if (odataProperty.type.startsWith('Edm')) {
// This is a primative type. Just return the field name.
return odataProperty.name;
} else {
// This is a complex type. Append default field.
const defaultField = ODataMetadataParser.defaultFields[odataProperty.type];
if (defaultField == null) {
throw new Error(`There is no default field for the OData datatype for ${field.name}`);
}
return `${odataProperty.name}/${defaultField.defaultFieldName}`;
}
}
/**
* @returns the OData formatted value.
* @param field The WIQL field of the value.
* @param value The WIQL value.
*/
private odataExpressionFromValue(field: WorkItemFieldReference, value: string): string {
// first, clean up any special values.
const macrosInValue = value.match(/^@([A-Za-z]+)/g) || [];
macrosInValue.forEach((macro) => {
switch (macro.toLocaleLowerCase()) {
case '@me':
value = value.replace(macro, this.webContext.user.email);
NotesService.instance.newNote('warning', `There is no OData equivalent for @me. Replacing with the static value of '${value}'.`);
break;
case '@project':
value = value.replace(macro, this.webContext.project.name);
NotesService.instance.newNote('warning', `There is no OData equivalent for @project. Replacing with the static value of '${value}'.`);
break;
case '@today':
const dateText = value.trim().toLocaleLowerCase();
if (dateText.length === '@today'.length) {
// If there are no arithmetic operations, we can use now().
value = 'date(now())';
} else {
// Arithmetic operations are currently not supported by VSTS OData. Replace with static value.
const matches = dateText.match(/@today ?([-+]) ?([0-9]+)/);
const theDate = new Date(Date.now());
theDate.setDate(theDate.getDate() + parseInt(`${matches[1]}${matches[2]}`, 10));
value = theDate.toISOString();
NotesService.instance.newNote('warning', `Arithmetic operations on @today are not supported by OData at this time. Replacing with the static value of '${value}'.`);
}
break;
default:
NotesService.instance.newNote('warning', `There is no OData equivalent for ${macro}. Please manually replace the value in your OData query.`);
break;
}
});
const oDataType = this.metadata[field.referenceName].type;
// Replace math operations for certain data types where math is allowed.
if (this.mathAllowedTypes.includes(oDataType)) {
value = value.replace(/ ?- ?/, ' sub ');
value = value.replace(/ ?\+ ?/, ' add ');
value = value.replace(/ ?\* ?/, ' mul ');
value = value.replace(/ ?\/ ?/, ' div ');
}
// Send to handler for type.
const handler = this.supportedTypesHandlingMap[oDataType];
return handler.handler(value);
}
//#region parseOperations
private parseSimpleOperator(clause: WorkItemQueryClause, operator: string): string {
const rhslhs = this.getLhsRhs(clause);
return `${rhslhs.left} ${operator} ${rhslhs.right}`;
}
private parseContains(clause: WorkItemQueryClause): string {
const rhslhs = this.getLhsRhs(clause);
return `contains(${rhslhs.left}, ${rhslhs.right})`;
}
private parseContainsWord(clause: WorkItemQueryClause): string {
const rhslhs = this.getLhsRhs(clause);
const rhs = rhslhs.right.substr(1).slice(0, -1); // remove quotes
return `(contains(${rhslhs.left}, '${rhs} ') or contains(${rhslhs.left}, ' ${rhs}'))`;
}
private parseEver(clause: WorkItemQueryClause): string {
const rhslhs = this.getLhsRhs(clause);
return `Revisions/any(r:r/${rhslhs.left} eq ${rhslhs.right})`;
}
private parseUnder(clause: WorkItemQueryClause): string {
const rhslhs = this.getLhsRhs(clause);
return `startswith(${rhslhs.left}, ${rhslhs.right})`;
}
private parseIn(clause: WorkItemQueryClause): string {
const lhs = this.odataExpressionFromField(clause.field);
const values = clause.value
.replace(/[\(\)]/, '')
.split(',')
.map((s) => s.trim());
if (values.length === 0) {
NotesService.instance.newNote('warning', `There was an issue parsing the in operator list for property ${clause.field.referenceName}. Leaving the clause out of $filter.`);
return null;
}
const orStatements = values.map((s) => `${lhs} eq ${this.odataExpressionFromValue(clause.field, s)}`).join(' or ');
return `(${orStatements})`;
}
//#endregion
//#region parseTypes
private validateAndThen(regexTest: RegExp, value: string, parser: () => string): string {
if (!regexTest.test(value)) {
throw new Error(`The value '${value}' is not formatted properly for the OData datatype`);
}
return parser();
}
private parseWithQuotesAndPrefix(value: string, prefix: string): string {
return `${prefix}'${value}'`;
}
private parseWithoutQuotes(value: string): string {
return value;
}
private parseDateTime(value: string): string {
const date = new Date(value);
return `datetime'${date.toISOString()}'`;
}
private parseDateTimeOffset(value: string): string {
if (value.includes('date(now())')) {
return value;
} else {
return new Date(value).toISOString();
}
}
private parseWithoutQuotesAndSuffix(value: string, suffix: string): string {
return `${value}${suffix}`;
}
private parseEmailUser(value: string): string {
value = (value.match(/<([^<>]+)>/) || [])[1] || value;
return `'${value}'`;
}
//#endregion
}

6
src/parsers/index.ts Normal file
Просмотреть файл

@ -0,0 +1,6 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
export * from './clause-parsers';
export * from './odata-parser';

273
src/parsers/odata-parser.ts Normal file
Просмотреть файл

@ -0,0 +1,273 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
*/
import { LinkQueryMode, LogicalOperation, QueryHierarchyItem, QueryRecursionOption, QueryType, WorkItemQueryClause } from 'TFS/WorkItemTracking/Contracts';
import { NotesService } from '../helpers';
import { IProperty, ODataMetadataParser } from '../models';
import { ClauseParser } from './clause-parsers';
interface ISelectAndExpand {
select: string;
expand: string;
}
/**
* Parse a WIQL query into an OData query.
*/
export class ODataParser {
/**
* The root QueryHeirarchyItem.
*/
private query: QueryHierarchyItem;
/**
* A mapping of Ref.ReferenceNames to IProperties from IMetadata.
*/
private metadata: { [id: string]: IProperty; };
/**
* An instance of the Clause Parser.
*/
private clauseParser: ClauseParser;
/**
* VSS Web Context.
*/
private webContext = VSS.getWebContext();
/**
*
* @param query The root QueryHeirarchyItem.
* @param metadata A mapping of Ref.ReferenceNames to IProperties from IMetadata.
*/
public constructor(query: QueryHierarchyItem, metadata: { [id: string]: IProperty; }) {
this.query = query;
this.metadata = metadata;
this.clauseParser = new ClauseParser(metadata);
}
/**
* Makes notes and returns a boolean denoting whether the query can be parsed or not.
*/
public isQueryValid(): boolean {
if (this.query.queryType === QueryType.Tree) {
NotesService.instance.newNote('error', `Only 'Flat list of work items' and 'Work items and direct links' queries are supported by OData.`);
return false;
}
if (this.query.isInvalidSyntax) {
NotesService.instance.newNote('error', `The WIQL query has invalid syntax. Fix the query before continuing.`);
return false;
}
return true;
}
/**
* @returns A full url OData query that matches the WIQL query.
*/
public makeAllStatements(): string {
if (this.query.queryType === QueryType.Flat) {
const selectAndExpand = this.makeSelectAndExpandStatement();
const statements = [
selectAndExpand.select,
selectAndExpand.expand,
this.makeFilterStatement(this.query.clauses),
this.makeOrderByStatement(),
].filter((column) => column != null).join('&');
return `https://analytics.dev.azure.com/${this.webContext.account.name}/_odata/v1.0/WorkItems?${statements}`;
} else if (this.query.queryType === QueryType.OneHop) {
NotesService.instance.newNote('info', `Non-Flat queries are not recommended by Azure DevOps Analytics. Expect warning VS403508 and possibly long response times.`);
const selectAndExpand = this.makeSelectAndExpandStatementForOneHop();
const statements = [
selectAndExpand.select,
selectAndExpand.expand,
this.makeFilterStatementForOneHop(),
this.makeOrderByStatement(),
].filter((column) => column != null).join('&');
return `https://analytics.dev.azure.com/${this.webContext.account.name}/_odata/v1.0/WorkItems?${statements}`;
}
}
public makeFilterStatementForOneHop(): string {
const filterStatement = this.makeFilterStatement(this.query.sourceClauses);
let linksQueryAdditionalClause: string = null;
if (this.query.filterOptions !== LinkQueryMode.LinksOneHopMayContain) {
// if not 'may contain', filter the base query down to only the work items in the target/link clause.
const additionalFilterStatements = [
this.makeFilterStatementSimple(this.query.targetClauses, 'l/TargetWorkItem'),
this.makeFilterStatementSimple(this.query.linkClauses, 'l'),
].filter((column) => column != null).join(' and ');
linksQueryAdditionalClause = additionalFilterStatements.length === 0 ? null :
`${this.query.filterOptions === LinkQueryMode.LinksOneHopDoesNotContain ? 'not ' : ''}Links/any(l: ${additionalFilterStatements})`;
}
return filterStatement != null ?
`${filterStatement}${linksQueryAdditionalClause != null ? ` and ${linksQueryAdditionalClause}` : ``}` :
linksQueryAdditionalClause != null ? `$filter=${linksQueryAdditionalClause}` : null;
}
public makeSelectAndExpandStatementForOneHop(): ISelectAndExpand {
const selectAndExpand = this.makeSelectAndExpandStatement();
if (this.query.filterOptions !== LinkQueryMode.LinksOneHopDoesNotContain) {
// include child expands when query requires them.
const childrenStatements = [selectAndExpand.select, selectAndExpand.expand].filter((column) => column != null).join('; ');
const filterStatements = [
this.makeFilterStatementSimple(this.query.targetClauses, 'TargetWorkItem'),
this.makeFilterStatementSimple(this.query.linkClauses),
].filter((column) => column != null).join(' and ');
const children = `Links($select=TargetWorkItem; ${filterStatements.length === 0 ? '' : `$filter=${filterStatements}; `}$expand=TargetWorkItem(${childrenStatements}))`;
return {
select: selectAndExpand.select,
expand: selectAndExpand.expand == null ? `$expand=${children}` : `${selectAndExpand.expand}, ${children}`,
};
}
return selectAndExpand;
}
/**
* @returns The $select statement, or null if it can't be parsed.
*/
public makeSelectAndExpandStatement(): ISelectAndExpand {
if (this.query.columns.length > 0) {
const expandList: string[] = [];
const selectList: string[] = [];
this.query.columns.forEach((column) => {
const odataProperty = this.metadata[column.referenceName];
if (odataProperty == null) {
NotesService.instance.newNote('warning', `Field ${column.referenceName} in the select statement does not have an OData equivalent. Leaving it out of $select.`);
} else {
if (!odataProperty.type.startsWith('Edm')) {
// Case: Complex type that has a default property (not expanded in the list of specialProperties).
const defaultField = ODataMetadataParser.defaultFields[odataProperty.type];
if (defaultField == null) {
NotesService.instance.newNote('warning', `The OData datatype of field ${column.referenceName} is not supported. Leaving it out of $select.`);
} else {
expandList.push(`${odataProperty.name}($select=${defaultField.defaultFieldName})`);
}
} else if (odataProperty.name.includes('/')) {
// Case: Complex type expanded in the list of specialProperties.
// Example: Area or Iteration.
const stringComponents = odataProperty.name.split('/');
expandList.push(`${stringComponents[0]}($select=${stringComponents[1]})`);
} else {
// Case: Primitive type.
selectList.push(odataProperty.name);
}
}
});
return {
select: (selectList.length === 0 ? null : `$select=${selectList.join(', ')}`),
expand: (expandList.length === 0 ? null : `$expand=${expandList.join(', ')}`),
};
} else {
NotesService.instance.newNote(
'info',
`every OData query should have a select statement. Please update your query to explicitly include a select statement.
Without a select statement, the default and unexpanded will be returned by the query.`
);
return null;
}
}
/**
* @returns The $orderby statement, or null if it can't be parsed.
*/
public makeOrderByStatement(): string {
if (this.query.sortColumns != null) {
const sortColumns = this.query.queryType === QueryType.Flat ? this.query.sortColumns : this.query.sortColumns.slice(this.query.sortColumns.length / 2);
const sortStatements = sortColumns.map((column) => {
const odataProperty = this.metadata[column.field.referenceName];
if (odataProperty == null) {
NotesService.instance.newNote('warning', `Property ${column.field.referenceName} in the order by statement does not have an OData equivalent. Leaving it out of $orderby.`);
} else if (!odataProperty.type.startsWith('Edm')) {
// Case: Complex type that has a default property (not expanded in the list of specialProperties).
const defaultField = ODataMetadataParser.defaultFields[odataProperty.type];
if (defaultField == null) {
NotesService.instance.newNote('warning', `The datatype of property ${column.field.referenceName} is not supported. Leaving it out of $orderby.`);
} else {
return `${odataProperty.name}/${defaultField.defaultFieldName} ${column.descending === true ? 'desc' : 'asc'}`;
}
} else {
return `${odataProperty.name} ${column.descending === true ? 'desc' : 'asc'}`;
}
return null;
});
return sortStatements.filter((s) => s != null).length === 0 ? null : `$orderby=${sortStatements.filter((s) => s != null).join(', ')}`;
}
return null;
}
/**
* @returns The $filter statement, or null if it can't be parsed.
*/
public makeFilterStatement(rootClause: WorkItemQueryClause, superTypePrefix: string = ''): string {
const oDataClauses = this.makeFilterStatementSimple(rootClause, superTypePrefix);
if (oDataClauses == null) {
return null;
}
// get ASOF. Unfortunately, the only way to currently detect ASOF is in the query text itself.
const basicTokenizedQuery = this.query.wiql.toLocaleLowerCase().split(' ');
const asofIndex = basicTokenizedQuery.findIndex((e) => e === 'asof');
if (asofIndex !== -1) {
// assuming thing after asof is a date. Assuming WIQL has been validated by ADO.
const dateString = basicTokenizedQuery[asofIndex + 1];
const isoDateString = new Date(dateString).toISOString();
return `$filter=Revisions/any(r: ${oDataClauses} and (r/ChangedDate le ${isoDateString}) and (r/RevisedDate ge ${isoDateString} or r/RevisedDate eq null))`;
}
return `$filter=${oDataClauses}`;
}
/**
* The filter statement, without ASOF and without $filter prepended.
*/
public makeFilterStatementSimple(rootClause: WorkItemQueryClause, superTypePrefix: string = ''): string {
if (rootClause == null) {
return null;
}
const oDataClauses = this.visitClauses(rootClause, superTypePrefix);
if (oDataClauses == null || oDataClauses === '' || oDataClauses === '()') {
return null;
}
return oDataClauses;
}
/**
* Recursively visits each clause.
*/
private visitClauses(clause: WorkItemQueryClause, superTypePrefix: string): string {
if (clause.logicalOperator != null) {
// We have an array of clauses all joined by the logical operator.
// Do DFS on the clauses.
const clausesString = clause.clauses
.map((c) => {
return this.visitClauses(c, superTypePrefix);
})
.filter((s) => s != null)
.join(` ${LogicalOperation[clause.logicalOperator]} `); // 'and' or 'or'
return clausesString.length === 0 ? null : `(${clausesString})`;
} else {
// We have a single clause. Parse it.
const parsedClause = this.clauseParser.parseClause(clause);
return parsedClause == null ? null : `${superTypePrefix != null && superTypePrefix !== '' ? `${superTypePrefix}/` : ``}${parsedClause}`;
}
}
}

78
src/polyfills.ts Normal file
Просмотреть файл

@ -0,0 +1,78 @@
// tslint:disable
import 'core-js/es6/array';
import 'core-js/es6/date';
import 'core-js/es6/function';
import 'core-js/es6/object';
import 'core-js/es6/promise';
import 'core-js/es6/regexp';
import 'core-js/es6/string';
import 'whatwg-fetch';
if (!String.prototype.includes) {
Object.defineProperty(String.prototype, 'includes', {
value: function (search, start) {
if (typeof start !== 'number') {
start = 0
}
if (start + search.length > this.length) {
return false
} else {
return this.indexOf(search, start) !== -1
}
}
})
}
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function (searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
if (sameValueZero(o[k], searchElement)) {
return true;
}
// c. Increase k by 1.
k++;
}
// 8. Return false
return false;
}
});
}

33
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
{
"compilerOptions": {
"baseUrl": "./",
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2016",
"dom"
],
"module": "es2015",
"moduleResolution": "node",
"paths": {
"@ms/configuration-environment": [
"config/environment/environment.dev"
],
},
"sourceMap": true,
"target": "es5",
"types": [
"vss-web-extension-sdk"
],
"jsx": "react"
},
"exclude": [
"node_modules",
"dist",
"**/*.spec.ts"
],
"compileOnSave": false
}

181
tslint.json Normal file
Просмотреть файл

@ -0,0 +1,181 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
{
"extends": [
"tslint:recommended",
"tslint-eslint-rules"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
// Custom - Check file header.
"file-header": [
true,
"Copyright \\(c\\) Microsoft Corporation"
],
"forin": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
200
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
// Custom - Disallow any type of assignment in conditionals.
"no-conditional-assignment": true,
// Custom - Disallow one or more blank lines in a row.
"no-consecutive-blank-lines": true,
"no-console": [
true,
"debug",
"info",
// Custom - Do not use console.log(...) method.
"log",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
// Custom - Disallow duplicate variable declarations in the same block scope.
"no-duplicate-variable": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
// Custom - Disallow iterating over an array with a for-in loop.
"no-for-in-array": true,
// Custom - Disable "no-implicit-dependencies" rule (otherwise, importing from e.g., "@esinsights/core" will be flagged).
"no-implicit-dependencies": false,
"no-inferrable-types": [
true,
"ignore-params"
],
// Custom - Disallow irregular whitespace within a file, including strings and comments.
"no-irregular-whitespace": true,
"no-misused-new": true,
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
// Custom - Disable submodules for "no-submodule-imports" rule (otherwise, importing from e.g., "@esinsights/core/diagnostics" will be flagged).
"no-submodule-imports": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
// Custom - Disallow unused imports, variables, functions and private class members.
"no-unused-variable": true,
"no-use-before-declare": true,
"no-var-keyword": true,
// Custom - Enforce consistent object literal property quote style.
"object-literal-key-quotes": [
true,
"consistent-as-needed"
],
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
// Custom - Disallow multiple variable definitions in the same declaration statement.
"one-variable-per-declaration": [
true,
"ignore-for-loop"
],
"prefer-const": true,
"quotemark": [
true,
"single",
// Custom - Allow to use the other quotemark in case escaping is required.
"avoid-escape"
],
"radix": true,
"semicolon": [
true,
"always"
],
// Custom - Custom trailing-comma rule.
"trailing-comma": [
true,
{
"multiline": {
"arrays": "always",
"exports": "never",
"functions": "never",
"imports": "never",
"objects": "always",
"typeLiterals": "always"
},
"singleline": "never"
}
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
// Custom - Enforce use of the isNan() function to check for NaN references.
"use-isnan": true,
// Custom (Override) - Check variable names for various errors.
"variable-name": [
true,
"ban-keywords",
"check-format"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

59
vss-extension-base.json Normal file
Просмотреть файл

@ -0,0 +1,59 @@
{
"manifestVersion": 1,
"name": "WIQL to OData",
"description": "Translates a WIQL query to OData",
"targets": [
{
"id": "Microsoft.VisualStudio.Services.Cloud"
}
],
"categories": "Azure Boards",
"icons": {
"default": "images/favicon.png"
},
"contributions": [],
"files": [
{
"path": "dist/bundles",
"addressable": true,
"packagePath": "bundles"
},
{
"path": "dist/index.html",
"addressable": true,
"packagePath": "index.html"
},
{
"path": "dist/dialog.html",
"addressable": true,
"packagePath": "dialog.html"
},
{
"path": "images",
"addressable": true
},
{
"path": "node_modules/vss-web-extension-sdk/lib",
"addressable": true,
"packagePath": "lib"
}
],
"content": {
"license": {
"path": "LICENSE"
},
"details": {
"path": "README.md"
}
},
"tags": [
"Analytics",
"Wiql",
"Query",
"OData"
],
"scopes": [
"vso.work",
"vso.analytics"
]
}