I am trying to utilize the library #gitgraph/js in my application (Note: I cannot use the React or NodeJS version, only the plain JS):
https://github.com/nicoespeon/gitgraph.js/tree/master/packages/gitgraph-js
Here is an example of what I am trying to do:
https://jsfiddle.net/Ben_Vins/fwcah5s0/7/
var myTemplateConfig = {
// … any specific template configuration
commit: {
shouldDisplayTooltipsInCompactMode: true, // default = true
tooltipHTMLFormatter: function(commit) {
return "<b>BV" + commit.sha1 + "</b>" + ": " + commit.message;
},
}
};
// Instantiate the graph.
const gitgraph = GitgraphJS.createGitgraph(graphContainer, {
mode: GitgraphJS.Mode.Compact,
template: new GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, myTemplateConfig ),
});
// Simulate git commands with Gitgraph API.
const master = gitgraph.branch("master");
master.commit("Initial commit");
const develop = gitgraph.branch("develop");
develop.commit("Add TypeScript");
const aFeature = gitgraph.branch("a-feature");
aFeature
.commit("Make it work")
.commit({ subject: "Make it right", hash: "test" })
.commit("Make it fast");
develop.merge(aFeature);
develop.commit("Prepare v1");
master.merge(develop).tag("v1.0.0");
By default, the result were too big, so I have applied a css to scale down the graph (the graph is an inline SVG without a cropbox property, so this is the only trick I could find).
What I would like to do:
Customize the tooltip of the onhover of the commits node (making it larger, change the text, change its css if possible)
Add a onclick event to capture the commit (in particular the commit hash to be used elsewhere in my application)
Extra points:
The documentation is limited and the examples from
https://github.com/nicoespeon/gitgraph.js/tree/master/packages/stories/src/gitgraph-js/
are in typescript. Are they also applicable for the JS version of gitgraph-js?
Note that the documentation of gitgraph.js seemed more detailed i.e. https://github.com/nicoespeon/gitgraph.js/blob/master/packages/gitgraph-js/MIGRATE_FROM_GITGRAPH.JS.md but I was trying to use the next version i.e. #gitgraph/js
Thanks for reaching out. I can add more colors to this as the author of the lib.
To customize the tooltip of a commit, you can provide a renderTooltip() custom function. It will give you the reference to the commit object, so you can customize at your will.
Same, you can pass a onClick() function that will give you the reference to the commit object
The examples are in TypeScript, but that's valid JS if you simply remove the types. Also, the gitgraph-js stories looks like they are React code, but they're not. They're simply wrapped in a React component so we could run them in Storybook along with the gitgraph-react ones.
With the latest version of the lib, you could try the following:
const graphContainer = document.getElementById("graph-container");
// Instantiate the graph.
const withoutBranchLabels = GitgraphJS.templateExtend(GitgraphJS.TemplateName.Metro, {
branch: { label: { display: false } },
});
const gitgraph = GitgraphJS.createGitgraph(graphContainer, {
mode: GitgraphJS.Mode.Compact,
template: withoutBranchLabels,
});
// Simulate git commands with Gitgraph API.
let storedCommit;
gitgraph
.commit({
subject: "Initial commit",
onClick: (commit) => storedCommit = commit
})
.commit({
subject: "Another commit",
onClick: (commit) => storedCommit = commit
})
.commit({
subject: "Do something crazy",
renderTooltip,
onClick: (commit) => storedCommit = commit
});
gitgraph
.branch("dev")
.commit({
subject: "Oh my god",
renderTooltip,
})
.commit({
subject: "This is a saxo!",
renderTooltip,
});
// Logs in console the sha1 of the clicked commit every 3s (for master branch only)
setInterval(() => storedCommit && console.log(`stored commit sha1: ${storedCommit.hashAbbrev}`), 3000)
// Custom tooltip renderer
function renderTooltip (commit) {
const commitSize = commit.style.dot.size * 2;
return createG({
translate: { x: commitSize + 10, y: commitSize / 2 },
children: [
createText({
fill: commit.style.dot.color,
content: `BV${commit.hashAbbrev}: ${commit.subject}`
})
],
});
}
// Helper functions to create SVGs
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
function createText(options) {
const text = document.createElementNS(SVG_NAMESPACE, "text");
text.setAttribute("alignment-baseline", "central");
text.setAttribute("dominant-baseline", "central");
text.textContent = options.content;
if (options.bold) {
text.setAttribute("font-weight", "bold");
}
if (options.fill) {
text.setAttribute("fill", options.fill);
}
if (options.font) {
text.setAttribute("style", `font: ${options.font}`);
}
if (options.anchor) {
text.setAttribute("text-anchor", options.anchor);
}
if (options.translate) {
text.setAttribute("x", options.translate.x.toString());
text.setAttribute("y", options.translate.y.toString());
}
if (options.onClick) {
text.addEventListener("click", options.onClick);
}
return text;
}
function createG(options) {
const g = document.createElementNS(SVG_NAMESPACE, "g");
options.children.forEach((child) => child && g.appendChild(child));
if (options.translate) {
g.setAttribute(
"transform",
`translate(${options.translate.x}, ${options.translate.y})`,
);
}
if (options.fill) {
g.setAttribute("fill", options.fill);
}
if (options.stroke) {
g.setAttribute("stroke", options.stroke);
}
if (options.strokeWidth) {
g.setAttribute("stroke-width", options.strokeWidth.toString());
}
if (options.onClick) {
g.addEventListener("click", options.onClick);
}
if (options.onMouseOver) {
g.addEventListener("mouseover", options.onMouseOver);
}
if (options.onMouseOut) {
g.addEventListener("mouseout", options.onMouseOut);
}
return g;
}
You can't just return HTML though, it must be SVG because the current renderer only handles SVG. That is surely less convenient than before, thus I encourage you to build helper functions like I did here. You can find helpers used in the stories too.
I hope that will be helpful. You can play with the new online playground too: https://codepen.io/nicoespeon/pen/arqPWb?editors=1010
Finally, I'm not maintaining the library much and I'm still looking for active maintainers: https://github.com/nicoespeon/gitgraph.js/issues/328
Related
EDIT : the former question was "JHipster Blueprint - How to get default Java package name ?"
I am developing a blueprint with JHipster that overrides the entity-server sub-generator. The desired behaviour is to replace all files in /src/main/java/defaultpackageName/domain/ from the project generated by the blueprint with my generated files. This is my code (files.js):
const entityServerFiles = {
noHibernate: [
//domain files
{
path: 'src/main/java/XXX/domain/',
templates: [
{
file: 'Entity.java',
renameTo: generator => `${generator.persistClass}.java`
}
]
}
]
};
function writeFiles() {
return {
write() {
this.writeFilesToDisk(entityServerFiles, this, false);
}
}
}
module.exports = {
writeFiles
};
For now it just creates a folder XXX in /src/main/java/ with my generated files in it.
What would I need to write in the XXX in path: 'src/main/java/XXX/domain/' in order to generate the files at the right place?
I did some digging on github on the generator-jhipster project and the prompt asking the user for the default java package name is in /generator-jhipster/generators/java/index.cjs/. This is the whole code https://github.com/jhipster/generator-jhipster/blob/main/generators/java/index.cjs
But I just took the important part:
const {
PACKAGE_NAME,
PACKAGE_NAME_DEFAULT_VALUE,
PRETTIER_JAVA_INDENT,
PRETTIER_JAVA_INDENT_DEFAULT_VALUE,
BUILD_TOOL,
BUILD_TOOL_DEFAULT_VALUE,
BUILD_TOOL_PROMPT_CHOICES,
} = require('./constants.cjs');
get prompting() {
return {
async showPrompts() {
if (this.shouldSkipPrompts()) return;
await this.prompt(
[
{
name: PACKAGE_NAME,
type: 'input',
validate: input => this.validatePackageName(input),
message: 'What is your default Java package name?',
default: () => this.sharedData.getConfigDefaultValue(PACKAGE_NAME, PACKAGE_NAME_DEFAULT_VALUE),
},
],
this.config
);
},
};
}
From what I understand, I just need to access the PACKAGE_NAME constant from my blueprint and it should work. Any ideas?
I just found the solution...
const entityServerFiles = {
noHibernate: [
//domain files
{
path: 'src/main/java/',
templates: [
{
file: 'package/domain/Entity.java',
renameTo: generator => `${generator.entityAbsoluteFolder}/domain/${generator.persistClass}.java`
}
]
}
]
};
function writeFiles() {
return {
write() {
this.writeFilesToDisk(entityServerFiles, this, false);
}
}
}
module.exports = {
writeFiles
};
The path property specifies the path inside the templates folder. Meanwhile, you can specify the path you want your files to be generated inside the project in the renameTo property.
So the answer to my question is ${generator.entityAbsoluteFolder} which had nothing to do with my original hypothesis, but this question can also be useful for writing templates in general.
I am utilising a Firebase Realtime Database with the below structure.
I wish to fetch all "notes" a user has access to and subscribe to changes in those notes.
notes: {
"noteId-1345" : {
"access" : {
"author": "1234567890"
"members": {
"1234567890": 0 <--- Author
"0987654321": 1 <--- Member
}
},
"data" : {
"title": "Hello",
"content": "Konichiwa!",
"comment": "123"
}
}
}
( I am aware this structure could, ideally, be more flat. :) )
To fetch all notes a user has access to - I keep an additional user_notes node in the root:
Whenever I associate a user (update of members) with a note, I update both /notes/$noteid and /user_notes/$uid.
user_notes: {
"$uid": {
"noteId-1345": {
myHide: false,
mySortOrder: 0,
title: "Hello"
}
}
}
When fetching data I wish to set up subscription to all notes the user has access to.
I begin by fetching the ids for notes the user has access to and then attach listeners to subscribe to updates in each note.
const uid = getState().auth.uid
let collectedNotes = {}
...
database.ref(`user_notes/${uid}`).on('value', (myAccessSnaps) => {
myAccessSnaps.forEach((accessSnap) => {
const noteId = accessSnap.key
const privateData = {'personalData': {...accessSnap.val()}}
database.ref(`notes/${noteId}`).on('value', (noteSnap)=>{
const notData = noteSnap.val()
const fullData = { ...privateData, ...notData }
const note = {
id: noteSnap.key,
...fullData
}
collectedNotes[note.id] = note
...
})
}))
})
(Of course, I will need to use .off() to detach listeners before setting up new ones)
This is somewhat problematic since I have to attach one listener per note - and there could be hundreds of notes in the database.
Is this the most efficient approach? - It seems inefficient.
Is there a way to listen to ALL the notes a user has acess to in the /notes path with one listener? Or is my approaching altogether wrong? :)
Kind regards /K
After understanding that .on() does not return a promise - I became much easier to solve my problem.
Attaching a lot of .on() listeners did not make any sense to me.
The easiest approach for me was to:
1 - Update my access nodes with a time stamp updatedAt each time a note was updated
2 - Load initial data using .once() that returns promise,see code below.
3 - Set up separate subscription for when access nodes changes
let myPromises = []
database.ref(`user_notes/${uid}`).once('value', (myAccessSnaps) => {
myAccessSnaps.forEach((accessSnap) => {
const noteId = accessSnap.key
const privateData = {'personalData': {...accessSnap.val()}}
myPromises.push(
database.ref(`notes/${noteId}`).once('value', (noteSnap)=>{
const notData = noteSnap.val()
const fullData = { ...privateData, ...notData }
const note = {
id: noteSnap.key,
...fullData
}
collectedNotes[note.id] = note
...
})
)
}))
})
return Promise.all(myPromises)
.then(() => {
dispatch(setNotes(categories))
...
// Set up subscription after initial load
database.ref(`user_notes/${uid}`).on('value', (myAccessSnaps) => {
...
// Use access node listener only - gets updated by 'updatedAt'
database.ref(`notes/${noteId}`).once('value', (noteSnap)=>{
//Collect and dispatch data
Kind regards /K
I am having a little trouble working out how to UNIT test with JEST and vanilla JS as nothign is really coming up online.
I am calling an API endpoint and then rendering that data as HTML - A set of UL/ LI's and a sub menu if there is a submenu.
How would you go about breaking this function up to unit test it - I am not even really sure where to start
Here is the data
"items":[
{
"label":"Work",
"url":"#/work",
"items":[
]
},
{
"label":"About",
"url":"#/about",
"items":[
{
"label":"What we do",
"url":"#/about/what-we-do"
},
{
"label":"How we work",
"url":"#/about/how-we-work"
},
{
"label":"Leadership",
"url":"#/about/leadership"
}
]
},
{
"label":"foo",
"url":"#/foo",
"items":[
{
"label":"Client Services",
"url":"#/foo/client"
},
{
"label":"Creative",
"url":"#/foo/creative"
},
{
"label":"Motion & Media",
"url":"#/foo/motion"
}
]
}
]
}
Here is the function I am calling to create the DOM elements - Its slightly messy but it is essentially creating the anchor tags and Ul / Li's.
createNavigationMenu: function (data) {
return data.map((item) => {
const listElement = document.createElement('li');
const listElementAnchor = document.createElement('a');
const subMenuContainer = document.createElement('ul');
const chevron = document.createElement('span');
listElementAnchor.setAttribute("href", `${item.url}`);
listElementAnchor.setAttribute("class", 'navigation__primary-menu-anchor');
listElementAnchor.innerHTML = item.label;
listElement.setAttribute("class", "navigation__primary-menu-item");
listElement.appendChild(listElementAnchor);
this.navigationContainer.append(listElement);
subMenuContainer.setAttribute("class", "navigation__submenu");
item.items.length ? listElement.append(subMenuContainer) : null
chevron.setAttribute("class", "arrow");
item.items.length ? listElementAnchor.append(chevron) : null
return item.items.map(submenuItem => {
const subMenuContainerItem = document.createElement('li');
const subMenuContainerItemAnchor = document.createElement('a');
subMenuContainerItemAnchor.setAttribute("href", `/${submenuItem.url}`);
subMenuContainerItemAnchor.setAttribute("class", 'navigation__submenu-menu-anchor');
subMenuContainerItemAnchor.innerHTML = submenuItem.label;
subMenuContainerItem.setAttribute("class", "navigation__submenu-menu-item");
subMenuContainerItem.append(subMenuContainerItemAnchor)
listElement.append(subMenuContainer);
subMenuContainer.append(subMenuContainerItem)
})
})
}
I have tried this with JSDOM but it does not seem to work
const data = {
"items": [
{
"label": "Work",
"url": "#/work",
"items": [
]
}
]
}
const markup = `<ul id="navigation__primary-menu" class="navigation__primary-menu">
<li>
<h1 class="navigation__primary-logo">HUGE </h1> <span id="iconSpan" class="saved"> </span>
</li>
<li class="navigation__primary-list-item"></li>
<li class="navigation__primary-menu-item">Work</li>
</ul>`
describe('should map data correctly', () => {
test('markup entered', () => {
const { windpw } = new JSDOM(markup);
const nav = Controller.createNavigationMenu(data)
expect(dom.serialize()).toMatch(nav)
});
});
This answer is for guidance only. It will not 'just work.'
Your request is complex - tests are usually written in Node (which is server side) and you are talking about using document and the DOM (which is client side.)
I would suggest looking into https://github.com/jsdom/jsdom which allows you to emulate document in Node.
Then, from their docs, something like:
const dom = new JSDOM();
and update your generator like this:
createNavigationMenu: function (data, dom) { //pass in your dom object here
return data.map((item) => {
const listElement = dom.createElement('li');
.... //replace all documents with dom
Pass your special dom into your generator function when running tests, and pass the true document at other times.
Finally, in your test (using jest example):
describe('dom test', () => {
it('should render like this', () => {
expect(dom.serialize()).toMatchSnapshot()
}
}
As an aside, DOM manipulation is usually only necessary in this day and age if you're trying to be super clever. I strongly suggest (if you haven't considered it) trying to use a framework that is going to make your life much easier. I use React, which also comes with the added bonus of testing frameworks that allow you to test rendering. Other frameworks are available.
I use parse-server-amazon-ses-email-adapter and would like to know how to localize the verification email and reset password email.
My clue is to check user's fields and then assign the correct template on server.js or AmazonSESAdapter.js. The problem is that user properties are empty, besides email, username.
For instance, at the example below, firstName is undefined.
Thanks.
emailAdapter: {
module: 'parse-server-amazon-ses-email-adapter',
options: {
// The address that your emails come from
fromAddress: 'Iron rr',
accessKeyId: 'gg',
secretAccessKey: 'gg',
region: 'eu-west-1',
// The template section
templates: {
passwordResetEmail: {
subject: 'Redefinir sua senha Iron Trainers',
pathPlainText: '/opt/bitnami/apps/parse/htdocs/node_modules/parse-server/node_modules/parse-server-amazon-ses-email-adapter/test/email-templates/password_reset_email.txt',
pathHtml: '/opt/bitnami/apps/parse/htdocs/node_modules/parse-server/node_modules/parse-server-amazon-ses-email-adapter/test/email-templates/password_reset_email.html',
callback: (user) => {
return {
firstName: user.get('firstName')
}
}
// Now you can use {{firstName}} in your templates
},
verificationEmail: {
subject: 'Confirmar email no Iron Trainers',
pathPlainText: '/opt/bitnami/apps/parse/htdocs/node_modules/parse-server/node_modules/parse-server-amazon-ses-email-adapter/test/email-templates/verification_email.txt',
pathHtml: '/opt/bitnami/apps/parse/htdocs/node_modules/parse-server/node_modules/parse-server-amazon-ses-email-adapter/test/email-templates/resendEmailVerification.html',
callback: (user) => {
return {
firstName: user.get('firstName')
}
}
// Now you can use {{firstName}} in your templates
},
customEmailAlert: {
subject: 'Urgent notification!',
pathPlainText: '/opt/bitnami/apps/parse/htdocs/node_modules/parse-server/node_modules/parse-server-amazon-ses-email-adapter/test/email-templates/custom_alert.txt',
pathHtml: '/opt/bitnami/apps/parse/htdocs/node_modules/parse-server/node_modules/parse-server-amazon-ses-email-adapter/test/email-templates/custom_alert.html',
}
}
As per my understanding this can be done but through a code change in the plugin.
There is a if-else condition at below line in code
https://github.com/ecohealthalliance/parse-server-amazon-ses-email-adapter/blob/0bce9b6c81681c3829a17b208d839d23c846ab05/src/AmazonSESAdapter.js#L90
Since you have not provided any feedback and I have not way to setup this, I have assume that the else part is what gets executed
const {
link,
appName,
user,
templateConfig
} = options;
const {
callback
} = templateConfig;
let userVars;
if (callback && typeof callback === 'function') {
userVars = callback(user);
// If custom user variables are not packaged in an object, ignore it
const validUserVars = userVars && userVars.constructor && userVars.constructor.name === 'Object';
userVars = validUserVars ? userVars : {};
}
pathPlainText = templateConfig.pathPlainText;
pathHtml = templateConfig.pathHtml;
templateVars = Object.assign({
link,
appName,
username: user.get('username'),
email: user.get('email')
}, userVars);
message = {
from: this.fromAddress,
to: user.get('email'),
subject: templateConfig.subject
};
}
Now in this part, 2 lines decide the template to be used
pathPlainText = templateConfig.pathPlainText;
pathHtml = templateConfig.pathHtml;
By this time, the callback you have provided has been called. Now in the callback you can set a variable, let assume it is name locale. So you can update the code like below
pathPlainText = templateConfig.pathPlainText + (userVars["locale"] || "en");
pathHtml = templateConfig.pathHtml + (userVars["locale"] || "en");
And then you will create templates which have the locale in the file path and with updated code the correct template will be picked.
You can also look at #bgran answer, at first look I do believe that should work as well
You'll need to do your localization in the template callback.
The callback is synchronous, so all of your localization will also need to be synchronous.
emailTemplate.html
<div>
{{localizedText}}
</div>
Other templates for each locale:
emailTemplate.en.html
<p>
Hi {{nome}}...
</p>
The emailer logic:
// The same templater used by parse-server-amazon-ses-email-adapter
import template from 'lodash.template'
/* ... */
const TemplatesByLocale = {
en: fs.readFileSync('./emailTemplate.en.html'),
}
verificationEmail: {
/* ... */
pathHtml: './path/to/emailTemplate.html',
callback: (user) => {
const locale = getLocaleSomehow(user) // needs to be synchronous
const localizedTemplate = TemplatesByLocale[locale]
const compiled = template(localizedTemplate, {
// same interpolation as parse-server-amazon-ses-email-adapter
interpolate: /{{([\s\S]+?)}}/g
})
const localizedText = compiled({
nome: user.get('nome'), /* ... */
})
return {
localizedText: localizedText,
}
},
/* ... */
}
It's worth noting that parse-server-amazon-ses-email-adapter will use the HTML template (the one specified via pathHtml) before it uses the plain text template, so if you've specified an HTML template you can just leave off the pathPlainText property.
I am trying to work on a custom jasmine reporter and get a list of all the failed specs in the specDone function:
specDone: function(result) {
if(result.status == 'failed') {
failedExpectations.push(result.fullName);
console.log(failedExpectations);
}
}
where failedExpectations will store an entire list of the failed specs and i need to access this in the afterLaunch function in the protractor config file. But due to the fact that the config file loads everytime a new spec runs it basically gets overwritten and scoping is such that I cannot access it in the afterLaunch function, that is where I am making the call to the slack api. Is there a way to achieve this?
This is what i have it based on : http://jasmine.github.io/2.1/custom_reporter.html
I think the best way is to post the results asynchronously after each spec (*or every "it" and "describe") using #slack/web-api. This way you don't have to worry about overwriting. Basically you "collect" all the results during the test run and send it before the next suite starts.
Keep in mind all of this should be done as a class.
First you prepare your you '#slack/web-api', so install it (https://www.npmjs.com/package/#slack/web-api).
npm i -D '#slack/web-api'
Then import it in your reporter:
import { WebClient } from '#slack/web-api';
And initialize it with your token. (https://slack.com/intl/en-pl/help/articles/215770388-Create-and-regenerate-API-tokens):
this.channel = yourSlackChannel;
this.slackApp = new WebClient(yourAuthToken);
Don't forget to invite your slack app to the channel.
Then prepare your result "interface" according to your needs and possibilities. For example:
this.results = {
title: '',
status: '',
color: '',
successTests: [],
fails: [],
};
Then prepare a method / function for posting your results:
postResultOnSlack = (res) => {
try {
this.slackApp.chat.postMessage({
text: `Suit name: ${res.title}`,
icon_emoji: ':clipboard:',
attachments: [
{
color: res.color,
fields: [
{
title: 'Successful tests:',
value: ` ${res.successTests}`,
short: false
},
{
title: 'Failed tests:',
value: ` ${res.fails}`,
short: false
},
]
}
],
channel: this.channel
});
console.log('Message posted!');
} catch (error) {
console.log(error);
}
When you got all of this ready it's time to "collect" your results.
So on every 'suitStart' remember to "clear" the results:
suiteStarted(result) {
this.results.title = result.fullName;
this.results.status = '';
this.results.color = '';
this.results.successTests = [];
this.results.fails = [];
}
Then collect success and failed tests:
onSpecDone(result) {
this.results.status = result.status
// here you can push result messages or whole stack or do both:
this.results.successTests.push(`${test.passedExpectations}`);
for(var i = 0; i < result.failedExpectations.length; i++) {
this.results.fails.push(test.failedExpectations[i].message);
}
// I'm not sure what is the type of status but I guess it's like this:
result.status==1 ? this.results.color = #DC143C : this.results.color = #048a04;
}
And finally send them:
suiteDone() {
this.postResultOnSlack(this.results);
}
NOTE: It is just a draft based on reporter of mine. I just wanted to show you the flow. I was looking at Jasmine custom reporter but this was based on WDIO custom reporter based on 'spec reporter'. They are all very similar but you probably have to adjust it. The main point is to collect the results during the test and send them after each part of test run.
*You can look up this explanation: https://webdriver.io/docs/customreporter.html
I highly recommend this framework, you can use it with Jasmine on top.