Switch Projects
Extending upon the "Save Note Stacks" recipe, this recipe allows you to seamlessly switch between entire projects. You can save your workspace state as a "project" and return to that exact state after working on something else. Concretely, to save a project means to save the current node stack, pinned nodes, shortcut script IDs, and theme.
INFO
Other note-taking apps usually provide the ability to switch between workspaces. However, while that helps keep notes organized and separated by project, the workspaces become siloed.
For instance, when you search for notes in one workspace, you won't be able to access notes from another workspace. This recipe gives you the best of both worlds - distinct workspaces that all belong to the same centralized knowledge base.
Project
tag
First, create a Project
tag by adding it to any note. Then, open the tag by clicking on it and add the following fields:
Theme
- Type:
String
- Type:
Active
- Type:
Boolean
- Type:
_saveProject
First, implement a private script called _saveProject
for saving the workspace state:
async function (projectName) {
const {
getContainer,
getNodeStack,
getSettings,
} = zeta.v1.context;
const {
paragraph,
bullet,
h1,
} = zeta.v1.element;
const {
getNoteIdByTitle,
getPinnedNodes,
getNodesByTag,
} = zeta.v1.query;
const {
updateNoteTag,
createNote,
updateNote,
tagNote,
} = zeta.v1.api;
const nodes = getNodesByTag("Project");
let activeProject = null;
let noteId = null;
// Fetch the currently active project.
for await (const node of nodes) {
if (
node.type === "Note" &&
node.tags["Project"]["Active"] &&
/^\[Project\]\s\w+/.test(node.title)
) {
activeProject = node;
break;
}
}
if (activeProject) {
const { id, title } = activeProject;
// If a project name wasn't passed
// in, overwrite the active project.
if (!projectName) {
projectName = title.slice(10);
noteId = id;
}
// Mark the active project as inactive.
updateNoteTag(id, "Project", {
"Active": false,
});
}
if (
!projectName ||
typeof projectName !== "string"
) {
throw new Error("Invalid project name.");
}
const title = `[Project] ${projectName}`;
noteId ||= await getNoteIdByTitle(title);
// If the project with the specified
// name doesn't exist yet, create it.
if (!noteId) {
noteId = createNote({ title });
tagNote(noteId, "Project");
}
// Begin saving the current workspace state.
const pinnedNodes = await getPinnedNodes();
const nodeStack = getNodeStack() || [
getContainer(),
];
const {
shortcutScriptIds,
theme,
} = getSettings();
const getCodeBullet = (node) => bullet({
text: `${node.type}:${node.id}`,
code: true,
});
updateNote(noteId, {
content: [
h1("Node stack"),
...nodeStack.map(getCodeBullet),
paragraph(""),
h1("Pinned nodes"),
...pinnedNodes.map(getCodeBullet),
paragraph(""),
h1("Shortcut script IDs"),
...shortcutScriptIds.map((id) => bullet({
code: true,
text: id,
})),
],
});
// Mark the saved project as active.
updateNoteTag(noteId, "Project", {
"Active": true,
"Theme": theme,
});
}
_loadProject
Then, implement another private script called _loadProject
for loading saved projects:
async function (projectName) {
const { openNodeStack } = zeta.v1.ui;
const {
getNoteIdByTitle,
getPinnedNodes,
getNodesByTag,
getNoteTags,
getNote,
} = zeta.v1.query;
const {
updateSettings,
updateNoteTag,
unpinNode,
pinNode,
} = zeta.v1.api;
if (
!projectName ||
typeof projectName !== "string"
) {
throw new Error("Invalid project name.");
}
const title = `[Project] ${projectName}`;
const noteId = await getNoteIdByTitle(title);
if (!noteId) {
throw new Error(
`Project not found: '${projectName}'.`
);
}
const nodes = getNodesByTag("Project");
let activeProject = null;
// Fetch the currently active project.
for await (const node of nodes) {
if (
node.type === "Note" &&
node.tags["Project"]["Active"] &&
/^\[Project\]\s\w+/.test(node.title)
) {
activeProject = node;
break;
}
}
if (activeProject) {
// If the project being loaded is
// currently active, do nothing.
if (activeProject.id === noteId) {
return;
}
// Mark the active project as inactive.
updateNoteTag(activeProject.id, "Project", {
"Active": false,
});
}
const [
{ content },
pinnedNodes,
tags,
] = await Promise.all([
getNote(noteId),
getPinnedNodes(),
getNoteTags(noteId),
]);
const props = {};
let current = null;
// Organize elements by heading.
content.forEach((element) => {
if (element.type === "Heading") {
current = [];
props[element.text] = current;
} else if (element.text) {
current.push(element.text);
}
});
// Begin loading the saved workspace state.
const nodeStack = props["Node stack"]
.map((text) => {
const [type, id] = text.split(":");
return { type, id };
});
openNodeStack(nodeStack);
pinnedNodes.forEach(unpinNode);
props["Pinned nodes"].forEach((text) => {
const [type, id] = text.split(":");
pinNode({ type, id });
});
updateSettings({
theme: tags["Project"]["Theme"],
shortcutScriptIds: props[
"Shortcut script IDs"
],
});
// Mark the loaded project as active.
updateNoteTag(noteId, "Project", {
"Active": true,
});
}
saveProject
Now that the private scripts have been prepared, implement the first public script called saveProject
that simply calls its private equivalent:
async function (projectName) {
await _saveProject(projectName);
}
Running saveProject("Project A")
in a code cell will save the current workspace state as "Project A".
listProjects
Similar to the "Save Note Stacks" recipe, implement a public script called listProjects
:
async function () {
const { getNodesByTag } = zeta.v1.query;
const { setOutput } = zeta.v1.ui;
const {
paragraph,
bullet,
} = zeta.v1.element;
const MAX_NUM_PROJECTS = 32;
const nodes = getNodesByTag("Project");
const projectNames = [];
let activeProjectName = null;
// Fetch the names of all saved projects.
for await (const node of nodes) {
if (
node.type === "Note" &&
/^\[Project\]\s\w+/.test(node.title)
) {
const projectName = node.title.slice(10);
projectNames.push(projectName);
if (node.tags["Project"]["Active"]) {
activeProjectName = projectName;
}
}
if (
projectNames.length >= MAX_NUM_PROJECTS
) {
break;
}
}
if (!projectNames.length) {
setOutput(paragraph(
"You don't have any projects."
));
return;
}
setOutput(projectNames.map((projectName) => {
return projectName === activeProjectName
? bullet({
text: projectName,
bold: true,
})
: bullet(projectName);
}));
}
Running listProjects()
in a code cell will list all saved projects.
loadProject
Finally, implement a public script called loadProject
:
async function (projectName) {
await _saveProject();
await _loadProject(projectName);
}
Running loadProject("Project A")
will load the saved workspace state for "Project A".
INFO
The reason why we implemented the two private scripts, _saveProject
and _loadProject
, is so that the public script loadProject
can save the workspace state of the currently active project before loading another project.
Next steps
It's possible to make switching projects even more convenient by creating scripts that are specific to each project. For example, you can create a public script called projectA
that switches to "Project A":
async function () {
await _saveProject();
await _loadProject("Project A");
}
Making this script a shortcut allows you to switch to "Project A" with the click of a button, without having to type loadProject("Project A")
in a code cell.