adding the code for the first time (#1)
* add the code * PR changes * reformat readme
This commit is contained in:
Родитель
1e901b8eef
Коммит
e44afaeb89
|
@ -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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"editor.detectIndentation": false,
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 4,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true
|
||||
}
|
26
README.md
26
README.md
|
@ -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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 4.4 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.4 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 27 KiB |
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
|
@ -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>
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
);
|
|
@ -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');
|
||||
}
|
|
@ -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>
|
|
@ -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[];
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*/
|
||||
|
||||
export * from './clause-parsers';
|
||||
export * from './odata-parser';
|
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
Загрузка…
Ссылка в новой задаче