mirror of
https://github.com/jonnydeates/obsidian-pivotal-tracker-integration-plugin.git
synced 2026-07-22 07:40:25 +00:00
Initial Push
This commit is contained in:
parent
41d2e6ae77
commit
bdf28d2d73
16 changed files with 4646 additions and 0 deletions
134
.gitignore
vendored
Normal file
134
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.idea
|
||||
.DS_Store
|
||||
24
README.md
Normal file
24
README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Obsidian Pivotal Tracker Integration Plugin
|
||||
|
||||
This is a plugin for Obsidian (https://obsidian.md), for the intergration between Pivotal Tracker (https://www.pivotaltracker.com/)
|
||||
This project uses Typescript to provide type checking and documentation.
|
||||
|
||||
## Setup the Plugin
|
||||
Once the plugin is installed, you will need to configure a few things.
|
||||
- First you need to get your Tracker API Key
|
||||
- This is inside your account settings
|
||||
- Then You need your Tracker Project ID
|
||||
- This can be found at the url of your tracker project.
|
||||
- Finally you need to specify a folder location for the stories to be pulled to
|
||||
|
||||
## How to use
|
||||
|
||||
- Enter settings within the plugin.
|
||||
-
|
||||
- Clone this repo.
|
||||
- `npm i` or `yarn` to install dependencies
|
||||
- `npm run dev` to start compilation in watch mode.
|
||||
|
||||
## Manually installing the plugin
|
||||
|
||||
- Copy over `main.js`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
|
||||
42
esbuild.config.mjs
Normal file
42
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from 'builtin-modules'
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === 'production');
|
||||
|
||||
esbuild.build({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ['main.ts'],
|
||||
bundle: true,
|
||||
external: [
|
||||
'obsidian',
|
||||
'electron',
|
||||
'@codemirror/autocomplete',
|
||||
'@codemirror/collab',
|
||||
'@codemirror/commands',
|
||||
'@codemirror/language',
|
||||
'@codemirror/lint',
|
||||
'@codemirror/search',
|
||||
'@codemirror/state',
|
||||
'@codemirror/view',
|
||||
'@lezer/common',
|
||||
'@lezer/highlight',
|
||||
'@lezer/lr',
|
||||
...builtins],
|
||||
format: 'cjs',
|
||||
watch: !prod,
|
||||
target: 'es2018',
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : 'inline',
|
||||
treeShaking: true,
|
||||
outfile: 'main.js',
|
||||
}).catch(() => process.exit(1));
|
||||
258
main.js
Normal file
258
main.js
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// main.ts
|
||||
var main_exports = {};
|
||||
__export(main_exports, {
|
||||
default: () => MyPlugin,
|
||||
retrieveStories: () => retrieveStories
|
||||
});
|
||||
module.exports = __toCommonJS(main_exports);
|
||||
var import_obsidian3 = require("obsidian");
|
||||
|
||||
// src/mappers/storyMapper.ts
|
||||
function mapStory(data) {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name.replace("/", " "),
|
||||
points: data.estimate,
|
||||
description: data.description,
|
||||
type: data.story_type,
|
||||
url: data.url,
|
||||
state: data.current_state
|
||||
};
|
||||
}
|
||||
function mapStories(data) {
|
||||
return data.map((d) => mapStory(d));
|
||||
}
|
||||
|
||||
// src/apiClient.ts
|
||||
var import_obsidian = require("obsidian");
|
||||
var TRACKER_URL = "https://www.pivotaltracker.com/services/v5";
|
||||
function generateConfig(trackerToken, projectId, queryParam) {
|
||||
return {
|
||||
method: "GET",
|
||||
url: `${TRACKER_URL}/projects/${projectId}/stories?${queryParam}`,
|
||||
headers: {
|
||||
"X-TrackerToken": trackerToken
|
||||
},
|
||||
contentType: "application/json"
|
||||
};
|
||||
}
|
||||
async function getStories(trackerId, trackerToken, included) {
|
||||
try {
|
||||
const projectId = parseInt(trackerId);
|
||||
const response = [];
|
||||
if (included.includeStories) {
|
||||
let unstartedStories = JSON.parse(await (0, import_obsidian.request)(generateConfig(trackerToken, projectId, "with_state=unstarted")));
|
||||
let startedStories = JSON.parse(await (0, import_obsidian.request)(generateConfig(trackerToken, projectId, "with_state=started")));
|
||||
response.push(...unstartedStories, ...startedStories);
|
||||
}
|
||||
if (included.includeChores) {
|
||||
let unstartedChores = JSON.parse(await (0, import_obsidian.request)(generateConfig(trackerToken, projectId, "with_story_type=chore&with_state=unstarted")));
|
||||
let startedChores = JSON.parse(await (0, import_obsidian.request)(generateConfig(trackerToken, projectId, "with_story_type=chore&with_state=started")));
|
||||
response.push(...unstartedChores, ...startedChores);
|
||||
}
|
||||
if (included.includeBugs) {
|
||||
let unstartedBugs = JSON.parse(await (0, import_obsidian.request)(generateConfig(trackerToken, projectId, "with_story_type=bug&with_state=unstarted")));
|
||||
let startedBugs = JSON.parse(await (0, import_obsidian.request)(generateConfig(trackerToken, projectId, "with_story_type=bug&with_state=started")));
|
||||
response.push(...unstartedBugs, ...startedBugs);
|
||||
}
|
||||
let stories = mapStories(response);
|
||||
return stories.filter((story) => {
|
||||
const onlyNonAccepted = story.state !== "accepted";
|
||||
if (included.pointed) {
|
||||
return onlyNonAccepted && typeof story.points === "number";
|
||||
}
|
||||
return onlyNonAccepted;
|
||||
});
|
||||
} catch (e) {
|
||||
return Promise.reject(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// src/writeOutputToFile.ts
|
||||
async function writeOutputToFile(folderPath, markdown, fileName) {
|
||||
const modifiedFileName = fileName.replace("/", " ");
|
||||
const readmePath = `${folderPath}/${modifiedFileName}.md`;
|
||||
return this.app.vault.create(readmePath, markdown);
|
||||
}
|
||||
|
||||
// src/generateMarkdown.ts
|
||||
var import_obsidian2 = require("obsidian");
|
||||
function storyToMarkdown(story) {
|
||||
let currentStory = appendLine(`### ${story.name} [#${story.id}](${story.url})`, newLine("## Pivotal Tracker"));
|
||||
if (story.type === "feature" && story.points) {
|
||||
currentStory = appendLine(newLine("#### Points: " + story.points), currentStory);
|
||||
}
|
||||
if (story.description) {
|
||||
currentStory = appendLine("### Description", currentStory);
|
||||
currentStory = appendLine(story.description, currentStory);
|
||||
}
|
||||
currentStory = appendLine(generateTags(story.type), currentStory);
|
||||
return currentStory;
|
||||
}
|
||||
function newLine(content) {
|
||||
return `${content}\r
|
||||
`;
|
||||
}
|
||||
function appendLine(content, destination) {
|
||||
return newLine(`${destination + content}`);
|
||||
}
|
||||
function generateTags(type) {
|
||||
return newLine("") + newLine("---") + newLine("tags") + newLine(" - pivotal-tracker") + newLine(" - " + type) + newLine("---");
|
||||
}
|
||||
function generateMarkdown(folderPath, stories) {
|
||||
stories.forEach(async (feature) => {
|
||||
await writeOutputToFile(folderPath, appendLine(storyToMarkdown(feature), ""), feature.name).then(() => {
|
||||
new import_obsidian2.Notice(`${feature.name} Created`);
|
||||
}).catch(() => {
|
||||
});
|
||||
});
|
||||
new import_obsidian2.Notice("Stories Created");
|
||||
}
|
||||
|
||||
// main.ts
|
||||
var DEFAULT_SETTINGS = {
|
||||
folderPath: "./stories",
|
||||
includeBugs: true,
|
||||
includeChores: true,
|
||||
includeStories: true,
|
||||
trackerUserId: "default",
|
||||
trackerAppId: "default",
|
||||
onlyPointedStories: false
|
||||
};
|
||||
var retrieveStories = async (settings) => {
|
||||
const { trackerUserId, trackerAppId, folderPath, onlyPointedStories, includeStories, includeChores, includeBugs } = settings;
|
||||
const inclusion = { includeStories, includeChores, includeBugs, pointed: onlyPointedStories };
|
||||
const stories = await getStories(trackerAppId, trackerUserId, inclusion);
|
||||
generateMarkdown(folderPath, stories);
|
||||
};
|
||||
var MyPlugin = class extends import_obsidian3.Plugin {
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
const ribbonIconEl = this.addRibbonIcon("book-open", "Pull Tracker Stories", (evt) => {
|
||||
retrieveStories(this.settings).then(() => {
|
||||
new import_obsidian3.Notice("Retrieving Stories");
|
||||
}).catch((e) => {
|
||||
new import_obsidian3.Notice(e + this.settings.trackerAppId + this.settings.trackerUserId);
|
||||
});
|
||||
});
|
||||
ribbonIconEl.addClass("my-plugin-ribbon-class");
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText("Status Bar Text");
|
||||
this.addCommand({
|
||||
id: "open-sample-modal-simple",
|
||||
name: "Open sample modal (simple)",
|
||||
callback: () => {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
});
|
||||
this.addCommand({
|
||||
id: "sample-editor-command",
|
||||
name: "Sample editor command",
|
||||
editorCallback: (editor, view) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection("Sample Editor Command");
|
||||
}
|
||||
});
|
||||
this.addCommand({
|
||||
id: "open-sample-modal-complex",
|
||||
name: "Open sample modal (complex)",
|
||||
checkCallback: (checking) => {
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(import_obsidian3.MarkdownView);
|
||||
if (markdownView) {
|
||||
if (!checking) {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
this.addSettingTab(new TrackerIntegrationSettingTab(this.app, this));
|
||||
this.registerDomEvent(document, "click", (evt) => {
|
||||
console.log("click", evt);
|
||||
});
|
||||
this.registerInterval(window.setInterval(() => console.log("setInterval"), 5 * 60 * 1e3));
|
||||
}
|
||||
onunload() {
|
||||
}
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
};
|
||||
var SampleModal = class extends import_obsidian3.Modal {
|
||||
constructor(app) {
|
||||
super(app);
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.setText("Woah!");
|
||||
}
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
};
|
||||
var TrackerIntegrationSettingTab = class extends import_obsidian3.PluginSettingTab {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "Settings for Tracker Integration" });
|
||||
new import_obsidian3.Setting(containerEl).setName("Tracker User Token").setDesc('To retrieve this, it can be found in your profile settings under "API KEY"').addText((text) => text.setPlaceholder("Enter your API Key").setValue(this.plugin.settings.trackerUserId).onChange(async (value) => {
|
||||
this.plugin.settings.trackerUserId = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new import_obsidian3.Setting(containerEl).setName("Tracker App ID").setDesc("This can be found in the project url bar https://www.pivotaltracker.com/n/projects/<your_project_id>").addText((text) => text.setPlaceholder("Enter your Apps Id").setValue(this.plugin.settings.trackerAppId).onChange(async (value) => {
|
||||
this.plugin.settings.trackerAppId = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new import_obsidian3.Setting(containerEl).setName("Folder Path").setDesc("This is the folder path to dump the stories it, currently they do NOT get created automatically.").addText((text) => text.setPlaceholder("./Path/Name").setValue(this.plugin.settings.folderPath).onChange(async (value) => {
|
||||
this.plugin.settings.folderPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new import_obsidian3.Setting(containerEl).setName("Only Pointed Stories").setDesc("Should only grab pointed stories if it is on.").addToggle((ev) => ev.setValue(this.plugin.settings.onlyPointedStories).onChange(async (value) => {
|
||||
this.plugin.settings.onlyPointedStories = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new import_obsidian3.Setting(containerEl).setName("Include Stories").setDesc("Should include stories if it is on.").addToggle((ev) => ev.setValue(this.plugin.settings.includeStories).onChange(async (value) => {
|
||||
this.plugin.settings.includeStories = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new import_obsidian3.Setting(containerEl).setName("Include Chores").setDesc("Should include chores if it is on.").addToggle((ev) => ev.setValue(this.plugin.settings.includeChores).onChange(async (value) => {
|
||||
this.plugin.settings.includeChores = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new import_obsidian3.Setting(containerEl).setName("Include Bugs").setDesc("Should include bugs if it is on.").addToggle((ev) => ev.setValue(this.plugin.settings.includeBugs).onChange(async (value) => {
|
||||
this.plugin.settings.includeBugs = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
};
|
||||
223
main.ts
Normal file
223
main.ts
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
import {addIcon, App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting} from 'obsidian';
|
||||
import getStories from "./src/apiClient";
|
||||
import generateMarkdown from "./src/generateMarkdown";
|
||||
// Remember to rename these classes and interfaces!
|
||||
|
||||
interface MyPluginSettings {
|
||||
folderPath: string;
|
||||
trackerUserId: string;
|
||||
trackerAppId: string;
|
||||
onlyPointedStories: boolean;
|
||||
includeChores: boolean;
|
||||
includeBugs: boolean;
|
||||
includeStories: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
folderPath: './stories',
|
||||
includeBugs: true,
|
||||
includeChores: true,
|
||||
includeStories: true,
|
||||
trackerUserId: 'default',
|
||||
trackerAppId: 'default',
|
||||
onlyPointedStories: false
|
||||
}
|
||||
|
||||
export const retrieveStories = async (settings: MyPluginSettings) => {
|
||||
const {trackerUserId, trackerAppId, folderPath, onlyPointedStories, includeStories, includeChores, includeBugs} = settings;
|
||||
const inclusion = {includeStories, includeChores, includeBugs, pointed: onlyPointedStories};
|
||||
const stories = await getStories(trackerAppId, trackerUserId, inclusion);
|
||||
|
||||
generateMarkdown(folderPath, stories);
|
||||
};
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
// This creates an icon in the left ribbon.
|
||||
const ribbonIconEl = this.addRibbonIcon('book-open', 'Pull Tracker Stories', (evt: MouseEvent) => {
|
||||
// Called when the user clicks the icon.
|
||||
|
||||
retrieveStories(this.settings).then(()=> {
|
||||
new Notice('Retrieving Stories');
|
||||
}).catch((e)=>{
|
||||
new Notice(e + this.settings.trackerAppId + this.settings.trackerUserId);
|
||||
});
|
||||
});
|
||||
// Perform additional things with the ribbon
|
||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText('Status Bar Text');
|
||||
|
||||
// This adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-simple',
|
||||
name: 'Open sample modal (simple)',
|
||||
callback: () => {
|
||||
new SampleModal(this.app).open();
|
||||
},
|
||||
|
||||
});
|
||||
// This adds an editor command that can perform some operation on the current editor instance
|
||||
this.addCommand({
|
||||
id: 'sample-editor-command',
|
||||
name: 'Sample editor command',
|
||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
||||
console.log(editor.getSelection());
|
||||
editor.replaceSelection('Sample Editor Command');
|
||||
}
|
||||
});
|
||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-complex',
|
||||
name: 'Open sample modal (complex)',
|
||||
checkCallback: (checking: boolean) => {
|
||||
// Conditions to check
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (markdownView) {
|
||||
// If checking is true, we're simply "checking" if the command can be run.
|
||||
// If checking is false, then we want to actually perform the operation.
|
||||
if (!checking) {
|
||||
new SampleModal(this.app).open();
|
||||
}
|
||||
|
||||
// This command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new TrackerIntegrationSettingTab(this.app, this));
|
||||
|
||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
||||
console.log('click', evt);
|
||||
});
|
||||
|
||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.setText('Woah!');
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class TrackerIntegrationSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', {text: 'Settings for Tracker Integration'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Tracker User Token')
|
||||
.setDesc('To retrieve this, it can be found in your profile settings under "API KEY"')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your API Key')
|
||||
.setValue(this.plugin.settings.trackerUserId)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.trackerUserId = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Tracker App ID')
|
||||
.setDesc('This can be found in the project url bar https://www.pivotaltracker.com/n/projects/<your_project_id>')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your Apps Id')
|
||||
.setValue(this.plugin.settings.trackerAppId)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.trackerAppId = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName('Folder Path')
|
||||
.setDesc('This is the folder path to dump the stories it, currently they do NOT get created automatically.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('./Path/Name')
|
||||
.setValue(this.plugin.settings.folderPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.folderPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Only Pointed Stories')
|
||||
.setDesc('Should only grab pointed stories if it is on.')
|
||||
.addToggle(ev => ev
|
||||
.setValue(this.plugin.settings.onlyPointedStories)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.onlyPointedStories = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Include Stories')
|
||||
.setDesc('Should include stories if it is on.')
|
||||
.addToggle(ev => ev
|
||||
.setValue(this.plugin.settings.includeStories)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.includeStories = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Include Chores')
|
||||
.setDesc('Should include chores if it is on.')
|
||||
.addToggle(ev => ev
|
||||
.setValue(this.plugin.settings.includeChores)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.includeChores = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
new Setting(containerEl)
|
||||
.setName('Include Bugs')
|
||||
.setDesc('Should include bugs if it is on.')
|
||||
.addToggle(ev => ev
|
||||
.setValue(this.plugin.settings.includeBugs)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.includeBugs = value;
|
||||
await this.plugin.saveSettings();
|
||||
}))
|
||||
|
||||
}
|
||||
}
|
||||
11
manifest.json
Normal file
11
manifest.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "obsidian-pivotal-tracker-integration-plugin",
|
||||
"name": "Pivotal Tracker Integration",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "This is an unofficial pivotal tracker integration plugin for Obsidian. This plugin allows the user to pull stories, chores, bugs from their pivotal counterpart.",
|
||||
"author": "Obsidian",
|
||||
"authorUrl": "https://github.com/JonnyDeates",
|
||||
"fundingUrl": "https://www.buymeacoffee.com/jondeates",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
3739
package-lock.json
generated
Normal file
3739
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "obsidian-pivotal-tracker-integration-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is an unofficial pivotal tracker integration plugin for Obsidian. (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.14.47",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
64
src/apiClient.ts
Normal file
64
src/apiClient.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import mapStories from './mappers/storyMapper';
|
||||
import Story from './types/story';
|
||||
import {request} from "obsidian";
|
||||
|
||||
const TRACKER_URL = 'https://www.pivotaltracker.com/services/v5';
|
||||
|
||||
function generateConfig(
|
||||
trackerToken: string,
|
||||
projectId: number,
|
||||
queryParam: string
|
||||
): { headers: { "X-TrackerToken": string }; method: string; contentType: string; url: string } {
|
||||
|
||||
return {
|
||||
method: 'GET',
|
||||
url: `${TRACKER_URL}/projects/${projectId}/stories?${queryParam}`,
|
||||
headers: {
|
||||
'X-TrackerToken': trackerToken,
|
||||
},
|
||||
contentType: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function getStories(
|
||||
trackerId: string,
|
||||
trackerToken: string,
|
||||
included: {includeStories: boolean, includeChores:boolean, includeBugs:boolean, pointed: boolean}
|
||||
): Promise<Story[]> {
|
||||
try {
|
||||
const projectId = parseInt(trackerId);
|
||||
|
||||
const response: any[] = [];
|
||||
|
||||
if(included.includeStories) {
|
||||
let unstartedStories = JSON.parse(await request(generateConfig(trackerToken, projectId, 'with_state=unstarted')));
|
||||
let startedStories = JSON.parse(await request(generateConfig(trackerToken, projectId, 'with_state=started')));
|
||||
response.push(...unstartedStories, ...startedStories);
|
||||
}
|
||||
if(included.includeChores) {
|
||||
let unstartedChores = JSON.parse(await request(generateConfig(trackerToken, projectId, 'with_story_type=chore&with_state=unstarted')));
|
||||
let startedChores = JSON.parse(await request(generateConfig(trackerToken, projectId, 'with_story_type=chore&with_state=started')));
|
||||
response.push(...unstartedChores, ...startedChores);
|
||||
}
|
||||
if(included.includeBugs) {
|
||||
let unstartedBugs = JSON.parse(await request(generateConfig(trackerToken, projectId, 'with_story_type=bug&with_state=unstarted')));
|
||||
let startedBugs = JSON.parse(await request(generateConfig(trackerToken, projectId, 'with_story_type=bug&with_state=started')));
|
||||
response.push(...unstartedBugs, ...startedBugs);
|
||||
}
|
||||
|
||||
let stories = mapStories(response as any[]);
|
||||
|
||||
|
||||
return stories.filter((story) => {
|
||||
const onlyNonAccepted = story.state !== 'accepted';
|
||||
|
||||
if(included.pointed){
|
||||
return onlyNonAccepted && typeof story.points === 'number'
|
||||
}
|
||||
|
||||
return onlyNonAccepted
|
||||
});
|
||||
} catch (e: any) {
|
||||
return Promise.reject(e.message);
|
||||
}
|
||||
}
|
||||
49
src/generateMarkdown.ts
Normal file
49
src/generateMarkdown.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import Story from './types/story';
|
||||
import writeOutputToFile from './writeOutputToFile';
|
||||
import {Notice} from "obsidian";
|
||||
|
||||
function storyToMarkdown(story: Story): string {
|
||||
|
||||
let currentStory = appendLine(`### ${story.name} [#${story.id}](${story.url})`, newLine("## Pivotal Tracker"),);
|
||||
|
||||
|
||||
if(story.type === "feature" && story.points ){
|
||||
currentStory = appendLine(newLine("#### Points: "+story.points), currentStory)
|
||||
}
|
||||
|
||||
if (story.description) {
|
||||
currentStory = appendLine("### Description", currentStory);
|
||||
currentStory = appendLine(story.description, currentStory);
|
||||
}
|
||||
|
||||
currentStory = appendLine(generateTags(story.type), currentStory);
|
||||
|
||||
return currentStory;
|
||||
}
|
||||
|
||||
function newLine(content: string) {
|
||||
return `${content}\r\n`
|
||||
}
|
||||
|
||||
function appendLine(content: string, destination: string): string {
|
||||
return newLine(`${destination + content}`);
|
||||
}
|
||||
|
||||
function generateTags(type: "feature" | "bug" | "chore"){
|
||||
|
||||
return newLine("") + newLine("---") + newLine("tags") + newLine(" - pivotal-tracker") + newLine(" - " + type) +newLine("---")
|
||||
|
||||
}
|
||||
|
||||
export default function generateMarkdown(folderPath: string, stories: Story[]) {
|
||||
stories.forEach(async (feature) => {
|
||||
await writeOutputToFile(folderPath, appendLine(storyToMarkdown(feature), ''), feature.name)
|
||||
.then(() => {
|
||||
new Notice(`${feature.name} Created`)
|
||||
})
|
||||
.catch(()=>{});
|
||||
});
|
||||
|
||||
new Notice('Stories Created')
|
||||
|
||||
}
|
||||
17
src/mappers/storyMapper.ts
Normal file
17
src/mappers/storyMapper.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import Story from '../types/story';
|
||||
|
||||
function mapStory(data: any): Story {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name.replace("/", " "),
|
||||
points: data.estimate,
|
||||
description: data.description,
|
||||
type: data.story_type,
|
||||
url: data.url,
|
||||
state: data.current_state,
|
||||
};
|
||||
}
|
||||
|
||||
export default function mapStories(data: any[]): Story[] {
|
||||
return data.map((d) => mapStory(d));
|
||||
}
|
||||
10
src/types/story.ts
Normal file
10
src/types/story.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export default interface Story {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
points: number | undefined;
|
||||
description: string;
|
||||
type: 'feature' | 'bug' | 'chore';
|
||||
url: string;
|
||||
state: 'accepted' | 'delivered' | 'started' | 'unstarted';
|
||||
}
|
||||
10
src/writeOutputToFile.ts
Normal file
10
src/writeOutputToFile.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export default async function writeOutputToFile(
|
||||
folderPath: string,
|
||||
markdown: string,
|
||||
fileName: string,
|
||||
): Promise<any> {
|
||||
const modifiedFileName = fileName.replace("/", " ");
|
||||
const readmePath = `${folderPath}/${modifiedFileName}.md`;
|
||||
|
||||
return this.app.vault.create(readmePath, markdown);
|
||||
}
|
||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES5",
|
||||
"ES6",
|
||||
"ES7"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { readFileSync, writeFileSync } from "fs";
|
||||
|
||||
const targetVersion = process.env.npm_package_version;
|
||||
|
||||
// read minAppVersion from manifest.json and bump version to target version
|
||||
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||
const { minAppVersion } = manifest;
|
||||
manifest.version = targetVersion;
|
||||
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||
|
||||
// update versions.json with target version and minAppVersion from manifest.json
|
||||
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||
versions[targetVersion] = minAppVersion;
|
||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue