Import jira (#1561)
* Load and parse xml * Priority and status * Replace foreach * type property * explicitArray false * Parse description html * Use turndown to convert html * Allow optional priority * Import assignee and reporter as Select * Store original URL * Created date * Created date * Update readme * .gitignore * Update readme * Update import readme * Fix readme * Update import/jira/README.md Fix typo. Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> * Remove commented out line * Add basic Jest test * Test that import was complete Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
This commit is contained in:
parent
95b230acea
commit
0e10f52317
11 changed files with 9914 additions and 1 deletions
|
@ -1,4 +1,10 @@
|
|||
# Import scripts
|
||||
|
||||
This subfolder contains scripts to import data from other systems. It is at an early stage. At present, there are examples of basic importing from Trello, Asana, and Notion. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
|
||||
This subfolder contains scripts to import data from other systems. It is at an early stage. At present, there are examples of basic importing from the following:
|
||||
* Trello
|
||||
* Asana
|
||||
* Notion
|
||||
* Jira
|
||||
* Todoist
|
||||
|
||||
[Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
|
||||
|
|
1
import/jira/.gitignore
vendored
Normal file
1
import/jira/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
test
|
23
import/jira/README.md
Normal file
23
import/jira/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Jira importer
|
||||
|
||||
This node app converts a Jira xml export into a Focalboard archive. To use:
|
||||
1. Open Jira advanced search, and search for all the items to export
|
||||
2. Select `Export`, then `Export XML`
|
||||
3. Save it locally, e.g. to `jira_export.xml`
|
||||
4. Run `npm install` from within `focalboard/webapp`
|
||||
5. Run `npm install` from within `focalboard/import/jira`
|
||||
6. Run `npx ts-node importJira.ts -i <path-to-jira.xml> -o archive.focalboard` (also from within `focalboard/import/jira`)
|
||||
7. In Focalboard, click `Settings`, then `Import archive` and select `archive.focalboard`
|
||||
|
||||
## Import scope and known limitations
|
||||
|
||||
Currently, the script imports each item as a card into a single board. Note that Jira XML export is limited to 1000 issues at a time.
|
||||
|
||||
Users are imported as Select properties, with the name of the user.
|
||||
|
||||
The following aren't currently imported:
|
||||
* Custom properties
|
||||
* Comments
|
||||
* Embedded files
|
||||
|
||||
[Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
|
16
import/jira/importJira.ts
Normal file
16
import/jira/importJira.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import minimist from 'minimist'
|
||||
import {run} from './jiraImporter'
|
||||
|
||||
async function main() {
|
||||
const args: minimist.ParsedArgs = minimist(process.argv.slice(2))
|
||||
|
||||
const inputFile = args['i']
|
||||
const outputFile = args['o'] || 'archive.focalboard'
|
||||
|
||||
return run(inputFile, outputFile)
|
||||
}
|
||||
|
||||
main()
|
53
import/jira/jiraImporter.test.ts
Normal file
53
import/jira/jiraImporter.test.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {run} from './jiraImporter'
|
||||
import * as fs from 'fs'
|
||||
import {ArchiveUtils} from '../../webapp/src/blocks/archive'
|
||||
|
||||
const inputFile = './test/jira-export.xml'
|
||||
const outputFile = './test/jira.focalboard'
|
||||
|
||||
describe('import from Jira', () => {
|
||||
test('import', async () => {
|
||||
const blockCount = await run(inputFile, outputFile)
|
||||
expect(blockCount === 4)
|
||||
})
|
||||
|
||||
test('import was complete', async () => {
|
||||
const archiveData = fs.readFileSync(outputFile, 'utf-8')
|
||||
const blocks = ArchiveUtils.parseBlockArchive(archiveData)
|
||||
|
||||
console.debug(blocks)
|
||||
|
||||
|
||||
blocks.forEach(block => {
|
||||
console.log(block.title)
|
||||
})
|
||||
|
||||
expect(blocks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
title: 'Jira import',
|
||||
type: 'board'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: 'Board View',
|
||||
type: 'view'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: 'Investigate feature area',
|
||||
type: 'card'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
title: 'Investigate feature',
|
||||
type: 'card'
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(outputFile)
|
||||
});
|
||||
})
|
243
import/jira/jiraImporter.ts
Normal file
243
import/jira/jiraImporter.ts
Normal file
|
@ -0,0 +1,243 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as fs from 'fs'
|
||||
import {exit} from 'process'
|
||||
import {ArchiveUtils} from '../../webapp/src/blocks/archive'
|
||||
import {Block} from '../../webapp/src/blocks/block'
|
||||
import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board'
|
||||
import {createBoardView} from '../../webapp/src/blocks/boardView'
|
||||
import {Card, createCard} from '../../webapp/src/blocks/card'
|
||||
import {createTextBlock} from '../../webapp/src/blocks/textBlock'
|
||||
import {Utils} from './utils'
|
||||
import xml2js, {ParserOptions} from 'xml2js'
|
||||
import TurndownService from 'turndown'
|
||||
|
||||
// HACKHACK: To allow Utils.CreateGuid to work
|
||||
(global.window as any) = {}
|
||||
|
||||
const optionColors = [
|
||||
'propColorGray',
|
||||
'propColorBrown',
|
||||
'propColorOrange',
|
||||
'propColorYellow',
|
||||
'propColorGreen',
|
||||
'propColorBlue',
|
||||
'propColorPurple',
|
||||
'propColorPink',
|
||||
'propColorRed',
|
||||
]
|
||||
let optionColorIndex = 0
|
||||
|
||||
var turndownService = new TurndownService()
|
||||
|
||||
async function run(inputFile: string, outputFile: string): Promise<number> {
|
||||
console.log(`input: ${inputFile}`)
|
||||
console.log(`output: ${outputFile}`)
|
||||
|
||||
if (!inputFile) {
|
||||
showHelp()
|
||||
}
|
||||
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`File not found: ${inputFile}`)
|
||||
exit(2)
|
||||
}
|
||||
|
||||
// Read input
|
||||
console.log(`Reading ${inputFile}`)
|
||||
const inputData = fs.readFileSync(inputFile, 'utf-8')
|
||||
|
||||
if (!inputData) {
|
||||
console.error(`Unable to read data from file: ${inputFile}`)
|
||||
exit(2)
|
||||
}
|
||||
|
||||
console.log(`Read ${Math.round(inputData.length / 1024)} KB`)
|
||||
|
||||
const parserOptions: ParserOptions = {
|
||||
explicitArray: false
|
||||
}
|
||||
const parser = new xml2js.Parser(parserOptions);
|
||||
const input = await parser.parseStringPromise(inputData)
|
||||
|
||||
if (!input?.rss?.channel) {
|
||||
console.error(`No channels in xml: ${inputFile}`)
|
||||
exit(2)
|
||||
}
|
||||
const channel = input.rss.channel
|
||||
const items = channel.item
|
||||
|
||||
// console.dir(items);
|
||||
|
||||
// Convert
|
||||
const blocks = convert(items)
|
||||
|
||||
// Save output
|
||||
// TODO: Stream output
|
||||
const outputData = ArchiveUtils.buildBlockArchive(blocks)
|
||||
fs.writeFileSync(outputFile, outputData)
|
||||
console.log(`Exported ${blocks.length} block(s) to ${outputFile}`)
|
||||
|
||||
return blocks.length
|
||||
}
|
||||
|
||||
function convert(items: any[]) {
|
||||
const blocks: Block[] = []
|
||||
|
||||
// Board
|
||||
const board = createBoard()
|
||||
board.rootId = board.id
|
||||
board.title = 'Jira import'
|
||||
|
||||
// Compile standard properties
|
||||
board.fields.cardProperties = []
|
||||
|
||||
const priorityProperty = buildCardPropertyFromValues('Priority', items.map(o => o.priority?._))
|
||||
board.fields.cardProperties.push(priorityProperty)
|
||||
|
||||
const statusProperty = buildCardPropertyFromValues('Status', items.map(o => o.status?._))
|
||||
board.fields.cardProperties.push(statusProperty)
|
||||
|
||||
const resolutionProperty = buildCardPropertyFromValues('Resolution', items.map(o => o.resolution?._))
|
||||
board.fields.cardProperties.push(resolutionProperty)
|
||||
|
||||
const typeProperty = buildCardPropertyFromValues('Type', items.map(o => o.type?._))
|
||||
board.fields.cardProperties.push(typeProperty)
|
||||
|
||||
const assigneeProperty = buildCardPropertyFromValues('Assignee', items.map(o => o.assignee?._))
|
||||
board.fields.cardProperties.push(assigneeProperty)
|
||||
|
||||
const reporterProperty = buildCardPropertyFromValues('Reporter', items.map(o => o.reporter?._))
|
||||
board.fields.cardProperties.push(reporterProperty)
|
||||
|
||||
const originalUrlProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: 'Original URL',
|
||||
type: 'url',
|
||||
options: []
|
||||
}
|
||||
board.fields.cardProperties.push(originalUrlProperty)
|
||||
|
||||
const createdDateProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: 'Created Date',
|
||||
type: 'date',
|
||||
options: []
|
||||
}
|
||||
board.fields.cardProperties.push(createdDateProperty)
|
||||
|
||||
blocks.push(board)
|
||||
|
||||
// Board view
|
||||
const view = createBoardView()
|
||||
view.title = 'Board View'
|
||||
view.fields.viewType = 'board'
|
||||
view.rootId = board.id
|
||||
view.parentId = board.id
|
||||
blocks.push(view)
|
||||
|
||||
for (const item of items) {
|
||||
console.log(
|
||||
`Item: ${item.summary}, ` +
|
||||
`priority: ${item.priority?._}, ` +
|
||||
`status: ${item.status?._}, ` +
|
||||
`type: ${item.type?._}`)
|
||||
|
||||
const card = createCard()
|
||||
card.title = item.summary
|
||||
card.rootId = board.id
|
||||
card.parentId = board.id
|
||||
|
||||
// Map standard properties
|
||||
if (item.priority?._) { setSelectProperty(card, priorityProperty, item.priority._) }
|
||||
if (item.status?._) { setSelectProperty(card, statusProperty, item.status._) }
|
||||
if (item.resolution?._) { setSelectProperty(card, resolutionProperty, item.resolution._) }
|
||||
if (item.type?._) { setSelectProperty(card, typeProperty, item.type._) }
|
||||
if (item.assignee?._) { setSelectProperty(card, assigneeProperty, item.assignee._) }
|
||||
if (item.reporter?._) { setSelectProperty(card, reporterProperty, item.reporter._) }
|
||||
|
||||
if (item.link) { setProperty(card, originalUrlProperty.id, item.link)}
|
||||
if (item.created) {
|
||||
const dateInMs = Date.parse(item.created)
|
||||
setProperty(card, createdDateProperty.id, dateInMs.toString())
|
||||
}
|
||||
|
||||
// TODO: Map custom properties
|
||||
|
||||
if (item.description) {
|
||||
const description = turndownService.turndown(item.description)
|
||||
console.log(`\t${description}`)
|
||||
const text = createTextBlock()
|
||||
text.title = description
|
||||
text.rootId = board.id
|
||||
text.parentId = card.id
|
||||
blocks.push(text)
|
||||
|
||||
card.fields.contentOrder = [text.id]
|
||||
}
|
||||
|
||||
blocks.push(card)
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
function buildCardPropertyFromValues(propertyName: string, allValues: string[]) {
|
||||
const options: IPropertyOption[] = []
|
||||
|
||||
// Remove empty and duplicate values
|
||||
const values = allValues.
|
||||
filter(o => !!o).
|
||||
filter((x, y) => allValues.indexOf(x) == y);
|
||||
|
||||
for (const value of values) {
|
||||
const optionId = Utils.createGuid()
|
||||
const color = optionColors[optionColorIndex % optionColors.length]
|
||||
optionColorIndex += 1
|
||||
const option: IPropertyOption = {
|
||||
id: optionId,
|
||||
value,
|
||||
color,
|
||||
}
|
||||
options.push(option)
|
||||
}
|
||||
|
||||
const cardProperty: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: propertyName,
|
||||
type: 'select',
|
||||
options
|
||||
}
|
||||
|
||||
console.log(`Property: ${propertyName}, values: ${values}`)
|
||||
|
||||
return cardProperty
|
||||
}
|
||||
|
||||
function setSelectProperty(card: Card, cardProperty: IPropertyTemplate, propertyValue: string) {
|
||||
const option = optionForPropertyValue(cardProperty, propertyValue)
|
||||
if (option) {
|
||||
card.fields.properties[cardProperty.id] = option.id
|
||||
}
|
||||
}
|
||||
|
||||
function setProperty(card: Card, cardPropertyId: string, propertyValue: string) {
|
||||
card.fields.properties[cardPropertyId] = propertyValue
|
||||
}
|
||||
|
||||
function optionForPropertyValue(cardProperty: IPropertyTemplate, propertyValue: string): IPropertyOption | null {
|
||||
const option = cardProperty.options.find(o => o.value === propertyValue)
|
||||
if (!option) {
|
||||
console.error(`Property value not found: ${propertyValue}`)
|
||||
return null
|
||||
}
|
||||
|
||||
return option
|
||||
}
|
||||
|
||||
function showHelp() {
|
||||
console.log('import -i <input.xml> -o [output.focalboard]')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
export { run }
|
9337
import/jira/package-lock.json
generated
Normal file
9337
import/jira/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
50
import/jira/package.json
Normal file
50
import/jira/package.json
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "focalboard-jira-importer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "importJira.js",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .tsx,.ts . --quiet --cache",
|
||||
"fix": "eslint --ext .tsx,.ts . --quiet --fix --cache",
|
||||
"test": "jest",
|
||||
"testRun": "ts-node importJira.ts -i test/jira_export.xml -o test/jira-import.focalboard",
|
||||
"debug:test": "node --inspect=5858 -r ts-node/register importJira.ts -i test/jira_export.xml -o test/jira-import.focalboard"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"jest": {
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"tsconfig": "./tsconfig.json"
|
||||
}
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"collectCoverage": true,
|
||||
"collectCoverageFrom": [
|
||||
"*.{ts,tsx,js,jsx}",
|
||||
"!test/**"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/node": "^14.14.28",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@types/xml2js": "^0.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.0",
|
||||
"@typescript-eslint/parser": "^4.15.0",
|
||||
"eslint": "^7.20.0",
|
||||
"jest": "^27.3.1",
|
||||
"ts-jest": "^27.0.7",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"turndown": "^7.1.1",
|
||||
"xml2js": "^0.4.23"
|
||||
}
|
||||
}
|
138
import/jira/test/jira-export.xml
Normal file
138
import/jira/test/jira-export.xml
Normal file
|
@ -0,0 +1,138 @@
|
|||
<!--
|
||||
RSS generated by JIRA (1001.0.0-SNAPSHOT#100183-sha1:347f6eac38020c8d2c450799ab145942a8caa5a4) at Tue Oct 19 19:29:39 UTC 2021
|
||||
|
||||
It is possible to restrict the fields that are returned in this document by specifying the 'field' parameter in your request.
|
||||
For example, to request only the issue key and summary add field=key&field=summary to the URL of your request.
|
||||
-->
|
||||
<!-- If you wish to do custom client-side styling of RSS, uncomment this:
|
||||
<?xml-stylesheet href="<base-url>/styles/jiraxml2html.xsl" type="text/xsl"?>
|
||||
-->
|
||||
<rss version="0.92">
|
||||
<channel>
|
||||
<title>Jira</title>
|
||||
<link>https://areca.atlassian.net/issues/?jql=text+%7E+%22Investigate%22</link>
|
||||
<description>An XML representation of a search request</description>
|
||||
<language>en-us</language>
|
||||
<issue start="0" end="2" total="2"/>
|
||||
<build-info>
|
||||
<version>1001.0.0-SNAPSHOT</version>
|
||||
<build-number>100183</build-number>
|
||||
<build-date>18-10-2021</build-date>
|
||||
</build-info>
|
||||
<item>
|
||||
<title>[AR-9] Investigate feature area</title>
|
||||
<link>https://areca.atlassian.net/browse/AR-9</link>
|
||||
<project id="10000" key="AR">Areca</project>
|
||||
<description></description>
|
||||
<environment></environment>
|
||||
<key id="10008">AR-9</key>
|
||||
<summary>Investigate feature area</summary>
|
||||
<type id="10001" iconUrl="https://areca.atlassian.net/secure/viewavatar?size=medium&avatarId=10318&avatarType=issuetype">Task</type>
|
||||
<priority id="3" iconUrl="https://areca.atlassian.net/images/icons/priorities/medium.svg">Medium</priority>
|
||||
<status id="10001" iconUrl="https://areca.atlassian.net/" description="">In Progress</status>
|
||||
<statusCategory id="4" key="indeterminate" colorName="yellow"/>
|
||||
<resolution id="-1">Unresolved</resolution>
|
||||
<assignee accountid="-1">Unassigned</assignee>
|
||||
<reporter accountid="557058:10df6720-08d0-4747-86f6-1aa1a7e45332">Chen Lim</reporter>
|
||||
<labels>
|
||||
</labels>
|
||||
<created>Fri, 24 Sep 2021 14:22:15 -0700</created>
|
||||
<updated>Fri, 24 Sep 2021 14:22:15 -0700</updated>
|
||||
<due></due>
|
||||
<votes>0</votes>
|
||||
<watches>1</watches>
|
||||
<attachments>
|
||||
</attachments>
|
||||
<subtasks>
|
||||
</subtasks>
|
||||
<customfields>
|
||||
<customfield id="customfield_10000" key="com.atlassian.jira.plugins.jira-development-integration-plugin:devsummarycf">
|
||||
<customfieldname>Development</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10019" key="com.pyxis.greenhopper.jira:gh-lexo-rank">
|
||||
<customfieldname>Rank</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>0|i0001r:</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10020" key="com.pyxis.greenhopper.jira:gh-sprint">
|
||||
<customfieldname>Sprint</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
</customfields>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>[AR-1] Investigate feature</title>
|
||||
<link>https://areca.atlassian.net/browse/AR-1</link>
|
||||
<project id="10000" key="AR">Areca</project>
|
||||
<description></description>
|
||||
<environment></environment>
|
||||
<key id="10000">AR-1</key>
|
||||
<summary>Investigate feature</summary>
|
||||
<type id="10002" iconUrl="https://areca.atlassian.net/secure/viewavatar?size=medium&avatarId=10307&avatarType=issuetype">Epic</type>
|
||||
<priority id="3" iconUrl="https://areca.atlassian.net/images/icons/priorities/medium.svg">Medium</priority>
|
||||
<status id="10000" iconUrl="https://areca.atlassian.net/" description="">To Do</status>
|
||||
<statusCategory id="2" key="new" colorName="blue-gray"/>
|
||||
<resolution id="-1">Unresolved</resolution>
|
||||
<assignee accountid="557058:10df6720-08d0-4747-86f6-1aa1a7e45332">Chen Lim</assignee>
|
||||
<reporter accountid="557058:10df6720-08d0-4747-86f6-1aa1a7e45332">Chen Lim</reporter>
|
||||
<labels>
|
||||
<label>PM</label>
|
||||
</labels>
|
||||
<created>Fri, 24 Sep 2021 14:19:44 -0700</created>
|
||||
<updated>Fri, 24 Sep 2021 14:21:32 -0700</updated>
|
||||
<due>Fri, 12 Nov 2021 00:00:00 +0000</due>
|
||||
<votes>0</votes>
|
||||
<watches>1</watches>
|
||||
<comments>
|
||||
<comment id="10000" author="557058:10df6720-08d0-4747-86f6-1aa1a7e45332" created="Fri, 24 Sep 2021 14:21:15 -0700" ><p>Kicking off Project Areca&#33; <20><></p></comment>
|
||||
</comments>
|
||||
<attachments>
|
||||
</attachments>
|
||||
<subtasks>
|
||||
</subtasks>
|
||||
<customfields>
|
||||
<customfield id="customfield_10000" key="com.atlassian.jira.plugins.jira-development-integration-plugin:devsummarycf">
|
||||
<customfieldname>Development</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10017" key="com.pyxis.greenhopper.jira:jsw-issue-color">
|
||||
<customfieldname>Issue color</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>blue</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10019" key="com.pyxis.greenhopper.jira:gh-lexo-rank">
|
||||
<customfieldname>Rank</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>0|hzzzzz:</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10020" key="com.pyxis.greenhopper.jira:gh-sprint">
|
||||
<customfieldname>Sprint</customfieldname>
|
||||
<customfieldvalues>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
<customfield id="customfield_10015" key="com.atlassian.jira.plugin.system.customfieldtypes:datepicker">
|
||||
<customfieldname>Start date</customfieldname>
|
||||
<customfieldvalues>
|
||||
<customfieldvalue>Tue, 28 Sep 2021 00:00:00 +0000</customfieldvalue>
|
||||
|
||||
</customfieldvalues>
|
||||
</customfield>
|
||||
</customfields>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
27
import/jira/tsconfig.json
Normal file
27
import/jira/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"target": "es2019",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"incremental": false,
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"exclude": [
|
||||
".git",
|
||||
"**/node_modules/*",
|
||||
"dist",
|
||||
"pack"
|
||||
]
|
||||
}
|
19
import/jira/utils.ts
Normal file
19
import/jira/utils.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
class Utils {
|
||||
static createGuid(): string {
|
||||
function randomDigit() {
|
||||
if (crypto && crypto.randomBytes) {
|
||||
const rands = crypto.randomBytes(1)
|
||||
return (rands[0] % 16).toString(16)
|
||||
}
|
||||
|
||||
return (Math.floor((Math.random() * 16))).toString(16)
|
||||
}
|
||||
return 'xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx'.replace(/x/g, randomDigit)
|
||||
}
|
||||
}
|
||||
|
||||
export { Utils }
|
Loading…
Reference in a new issue