Welcome to Aha! Develop. In this article, we will walk you through how to build an extension so you can favorite features in Aha! Develop. Along the way, you'll learn how to add a Favorite button to your features and how to build an extension-specific page to show all the favorited features. Finally, you'll learn how to remove your extension if you no longer want it in your account.
Before you proceed, make sure:
You already have an Aha! Develop account
You have installed the Aha! Develop extension CLI
Click any of the following links to skip ahead:
Getting started
Create an extension
Navigate to your code directory. In your terminal, run:
aha extension:create
This will create a new folder in your current directory based on the information you put in.
Here are some recommended values:
// Give your extension a human readable name: favorites
// Who are you? Your personal or organization github handle is a good identifier: tutorial
// Each extension must have a universally unique identifer that is also a valid NPM package name.
// Generally a good identifier is <organization-name>.<extension-name>.
// Extension identifier: <yourhandle>-tutorial.favorites
// Creating... Extension created in directory 'favorites'
In your terminal, step into the newly created directory.
cd favorites
Note: The rest of the article will reference <yourhandle>-tutorial.favorites . When you see this, add the one you specified above during the creation step.
Authorize your extension
Before you can install your extension, the extensions CLI needs to know where to install it. To set this, you need to authorize your extension.
In the terminal, run:
aha auth:login
This will open up a web browser and ask you to log in to your account.
Opening browser to login to Aha! and authorize the CLI
Waiting for login... complete.
Testing login... Success!
Once you have done this, you are ready to install.
Install your extension
To install your extension, in the terminal, run:
aha extension:install
// Installing extension '<yourhandle>-tutorial.favorites' to 'https://<yourorg>.aha.io:443'
// contributes views: 'samplePage'
// contributes commands: 'sampleCommand'
// Compiling... done
// Uploading... done
Navigate to /develop/settings/account/extensions. You will see your extension listed.
Establish your feedback loop
In this section, you will learn how to get situated with your extension and establish a feedback loop.
Add a debugger
One of the most useful techniques is debugging in production.
Hop into src/commands/sampleCommand.js
Update the contents:
aha.on("sampleCommand", () => {
debugger;
console.log("hello from command!")
});
Save. Then, in your console, run:
aha extension:install
To see your debugger in action:
In your Aha! Develop account, then navigate to Work → My Board.
Open up your dev tools in your browser.
Click on the search icon in the top-right corner of the page.
Click on the Commands tab.
Select Sample command.
You will now see your debugger.
Use watch
Instead of running aha extension:install every time you want to see an update, you can instead run:
aha extension:watch
This will take your local code and run it remotely so you can hit Save and see it live.
Try it out. We will update the name of the command:
In package.json , change the name from Sample command to Royal command and hit Save.
"commands": {
"sampleCommand": {
"title": "Royal command",
"entryPoint": "src/commands/sampleCommand.js"
}
}
Hop into your Aha! Develop account and reopen the command palette via the search icon. You will now see the updated name.
Keep an eye on the terminal you are running this in. It will emit helpful errors in case there are any typos or problems compiling.
The page contribution
There is one other contribution generated by default.
"views": {
"samplePage": {
"title": "Sample page",
"host": "page",
"entryPoint": "src/views/samplePage.js",
"location": {
"menu": "Work"
}
}
},
To see this page, hover the Work option in the navigation bar. Select Sample page at the bottom to create a Favorites page.
Add the favorite button
In this section, you will add the Favorite button to your features.
Update the contribution
First, we'll add the favorite button's field to the feature drawer.
In package.json, add a new entry inside views :
//underneath "samplePage":
"addFavorite": {
"title": "Favoriting",
"host": "attribute",
"entryPoint": "src/views/addFavorite.js",
"recordTypes": [
"Feature"
]
}
This specifies we want to make an extension field labeled Favoriting available to our features.
To specify what shows up in this field, create src/views/addFavorite.js and add:
import React from "react";
aha.on("addFavorite", ({record, fields}, { settings }) => {
return (
<>
<div>Add Favorite</div>
</>
);
});
Now we will tell Aha! Develop to show this field:
Navigate to Work → My Board.
Click on a feature card to open the drawer. Create one with any name if you need.
Click the More options button and select Add custom field.
Click on the Extension fields section.
Drag the Favoriting field onto your feature.
Hit Save layout.
Use extension fields to store favorited
To store whether or not a feature is favorited, we are going to use extension fields. Extension fields are scoped per record. You can store whatever you want in here.
To save whether or not a record is favorited, we are going to store a boolean value inside a property named favorited :
{
"favorited": true
}
Here is the JavaScript to accomplish this.
const favorited = true;
await record.setExtensionField("<yourhandle>-tutorial.favorites", "favorited", favorited);
However, you cannot just pop this inside src/views/addFavorite.js as-is. We are going to need to track if a record is currently favorited. To track this information, we are going to need to store state with a component.
Pull into a component
Update src/views/addFavorite.js to this:
import React from "react";
const AddFavorite = () => {
return (
<>
<span>Add Favorite</span>
</>
);
}
aha.on("addFavorite", ({record, fields}, { settings }) => {
return (
<AddFavorite />
);
});
Once you save, you should now see this in your features drawer:
Writing favorites data
To write favorites data, we will:
Pass record into our component so we can use it to write data.
Store whether this record is favorited via useState .
Provide a click handler to update whether it is favorited.
Toggle the text to specify Add or Remove options based on favorited state.
import React, { useState, useEffect } from "react";
const AddFavorite = (props) => {
const record = props.record
const [favorited, setFavorited] = useState(false);
async function toggleFavorited() {
await record.setExtensionField("<yourhandle>-tutorial.favorites", "favorited", !favorited);
setFavorited(!favorited);
}
return (
<>
<span onClick={toggleFavorited}>
{
favorited
? 'Remove favorite'
: 'Add favorite'
}
</span>
</>
);
}
aha.on("addFavorite", ({record, fields}, { settings }) => {
return (
<AddFavorite record={record} />
);
});
Read favorites data
Now, one last piece of functionality and our field will be ready. When we open the drawer, we need to check to see if the feature is already favorited or not. To do this, we can use a useEffect hook to set the favorited value and have it run once by specifying [] as the second argument to the effect.
useEffect(() => {
async function fetchFavorited() {
const favorited = await record.getExtensionField("<yourhandle>-tutorial.favorites", "favorited");
setFavorited((favorited || false));
};
fetchFavorited();
}, [])
Finally, we can weave in some loading state. We will look at a more sophisticated way of doing this shortly.
In src/views/addFavorite.js :
import React, { useState, useEffect } from "react";
const AddFavorite = (props) => {
const record = props.record
const [favorited, setFavorited] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchFavorited() {
const favorited = await record.getExtensionField("<yourhandle>-tutorial.favorites", "favorited");
setFavorited((favorited || false));
setLoading(false)
};
fetchFavorited();
}, [])
async function toggleFavorited() {
await record.setExtensionField("<yourhandle>-tutorial.favorites", "favorited", !favorited);
setFavorited(!favorited);
}
if (loading) {
return (
<>
<span>Loading Favorite</span>
</>
)
}
return (
<>
<span onClick={toggleFavorited}>
{
favorited
? 'Remove favorite'
: 'Add favorite'
}
</span>
</>
);
}
aha.on("addFavorite", ({record, fields}, { settings }) => {
return (
<AddFavorite record={record} />
);
});
Read favorites data better
It may feel inefficient to make a new network request for data about a feature we have just loaded. Luckily, there is a much easier way. The extension data is already available to us.
In src/views/addFavorite.js :
aha.on("addFavorite", ({record, fields}, { settings }) => {
debugger;
return (
<AddFavorite record={record} />
);
});
Click on a feature you have already favorited to open the drawer, hit this debugger, and take a look at fields:
{ favorited: true }
It already has the extension data ready for us. Remove the debugger.
We can now drastically simplify our component, removing the fetch, and updating src/views/addFavorite.js :
import React, { useState, useEffect } from "react";
const AddFavorite = (props) => {
const record = props.record;
const fields = props.fields;
const [favorited, setFavorited] = useState(fields.favorited || false);
async function toggleFavorited() {
await record.setExtensionField("<yourhandle>-tutorial.favorites", "favorited", !favorited);
setFavorited(!favorited);
}
return (
<>
<span onClick={toggleFavorited}>
{
favorited
? 'Remove favorite'
: 'Add favorite'
}
</span>
</>
);
}
aha.on("addFavorite", ({record, fields}, { settings }) => {
return (
<AddFavorite record={record} fields={fields} />
);
});
Favorite a couple features for the next section.
Add the favorites page
Now that we are tracking extension data on our records, we will aggregate it onto a single page.
Update the contribution again
Let's update our Sample page to something more sensible. In package.json , we will update the values to:
"views": {
"viewFavorites": {
"title": "Favorites",
"host": "page",
"entryPoint": "src/views/viewFavorites.js",
"location": {
"menu": "Work"
}
},
"addFavorite": {
/** SNIP! **/
},
Then we will rename src/views/samplePage.js to src/views/viewFavorites.js and update the contents to:
import React from "react";
aha.on("viewFavorites", ({record, fields}, { settings }) => {
return (
<>
<h1> Favorites </h1>
</>
);
});
Refresh your page. You will now be able to navigate to your newly defined content.
We need a component here as well in order to hold state about the favorited features. We will use the same pattern as before in src/views/viewFavorites.js :
import React from "react";
const ViewFavorites = () => {
return (
<>
<h1> Favorites </h1>
</>
)
}
aha.on("viewFavorites", ({record, fields}, { settings }) => {
return (
<>
<ViewFavorites />
</>
);
});
Find GraphQL Explorer
To get at our data, we will construct a GraphQL query using the GraphQL explorer.
In the top-right corner of the page, select Setting ⚙️ → Personal → Developer.
Click on the GraphQL API Explorer tab to open a new tab.
Alternatively:
Navigate to https://<your-org>.aha.io/settings/api_keys .
Click on the GraphQL API Explorer tab to open a new tab.
Build the query
We want to grab all the features in our project that are favorited. First, we will start by grabbing all the features in our project. Then, we will elaborate the query to also return our extension fields.
In the main query portion in the top left, we will pass in a new $projectId query variable and tell GraphQL that this data is an ID . Then, we will filter our features by the current $projectId so users can see favorites per project.
query($projectId: ID) {
features(filters: {projectId: $projectId}, order: [{name: createdAt, direction: DESC}]) {
nodes {
id
name
referenceNum
}
}
}
To specify what value will be used in $projectId , add a value to the bottom Query Variables section:
{
"projectId":"6956211859773302040"
}
To get your projectId, flip over to a tab with your project open, such as your newly added Favorites page. In the console, write:
aha.project.id
// -> "6956211859773302040"
Hit Run and you should see your features:
To get our extension data, we will specify that, on our feature, we would also like to see extensionFields.
query($projectId: ID) {
features(filters: {projectId: $projectId}, order: [{name: createdAt, direction: DESC}]) {
nodes {
id
name
referenceNum
extensionFields {
name
value
}
}
}
}
Hit Run again and you should now see the favorites data.
Use aha.graphQuery
Now that we have a working query, we will use it to put data on our Favorites page. This time, we will store all information about our query in a single piece of state.
In src/views/viewFavorites.js :
import React, {useState, useEffect} from "react";
const initialQueryState = {
loading: false,
data: null
};
const ViewFavorites = () => {
const [favoritesQuery, setFavoritesQuery] = useState(initialQueryState);
return (
<>
<h1> Favorites </h1>
</>
)
}
Next, we will set up our Query and our Variables just like in the GraphQL Explorer.
const ViewFavorites = () => {
const [favoritesQuery, setFavoritesQuery] = useState(initialQueryState);
useEffect(() => {
async function fetchFavorites() {
const query = `
query($projectId: ID) {
features(filters: {projectId: $projectId}, order: [{name: createdAt, direction: DESC}]) {
nodes {
id
name
referenceNum
extensionFields {
name
value
}
}
}
}
`;
const variables = {
projectId: aha.project.id
};
setFavoritesQuery({...favoritesQuery, loading: true});
const data = await aha.graphQuery(query, { variables } );
setFavoritesQuery({ loading: false, data });
};
fetchFavorites();
}, [])
return (
<>
<h1> Favorites </h1>
</>
)
}
The value stored in favoritesQuery.data will be identical to the data you see in your GraphQL Explorer for the query.
Display the features
Now that we are retrieving our features, we will show them. In src/views/viewFavorites.js :
const ViewFavorites = () => {
/** Snip! **/
if (favoritesQuery.loading) {
return (
"Loading..."
)
}
if (!favoritesQuery.loading && favoritesQuery?.data !== null) {
return (
<>
<h1> Favorites </h1>
<ul>
{favoritesQuery.data.features.nodes.map((favorite) =>
(
<li>
{favorite.referenceNum} - {favorite.name}
</li>
)
)}
</ul>
</>
);
}
return ("Error!");
}
Filter the features
Currently, we are showing all features. To only show those that are favorites, in src/views/viewFavorites.js :
const ViewFavorites = () => {
/** Snip! **/
if (!favoritesQuery.loading && favoritesQuery?.data !== null) {
const favoritedFeatures = favoritesQuery.data.features.nodes.filter((feature) => {
const favorited = feature.extensionFields.find(field => field.name === "favorited");
if (!favorited) return false;
return favorited.value;
});
return (
<>
<h1> Favorites </h1>
<ul>
{favoritedFeatures.map((favorite) =>
(
<li>
{favorite.referenceNum} - {favorite.name}
</li>
)
)}
</ul>
</>
);
}
/** Snip! **/
}
Make features links
To make the features a little more useful, let's make them links. In src/views/viewFavorites.js :
{favoritedFeatures.map((favorite) =>
(
<li>
<a href={`/features/${favorite.id}`}>
{favorite.referenceNum} - {favorite.name}
</a>
</li>
)
)}
With two components and two contributions, you have now created project-wide favoriting and learned how to enter an extension development workflow. You're almost ready to start introducing your own extensions into your development tool. But before you do, let's clean up this tutorial extension.
Delete your extension
In your terminal, run:
aha extension:uninstall
// -> Uninstalling... done