Skip to content

Commit

Permalink
Actions Outline (#56)
Browse files Browse the repository at this point in the history
* Action Outline
  • Loading branch information
wandyezj authored Jun 7, 2024
1 parent 78364a2 commit cd472cb
Show file tree
Hide file tree
Showing 9 changed files with 766 additions and 34 deletions.
1 change: 1 addition & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// words - list of words to be always considered correct
// prettier-ignore
"words": [
"Addin",
"appsforoffice",
"CODEOWNERS",
"endregion",
Expand Down
10 changes: 10 additions & 0 deletions docs/features/actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Actions

Associate a trigger with an action.

- Trigger
- office.js event
- shortcut
- Action
- executable JavaScript

234 changes: 202 additions & 32 deletions src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,211 @@
// cspell:ignore SHOWTASKPANE HIDETASKPANE addin
import { ActionType, TriggerAction, TriggerType } from "./core/actions/TriggerAction";
import { registerTriggerActionsInitial, setTriggerActions } from "./core/actions/triggerActionHandlers";
import { getSourceEmbed } from "./core/embed/getSourceEmbed";
import { setHost } from "./core/globals";

console.log("actions load");

Office.actions.associate("SHOWTASKPANE", function () {
console.log("shortcut - Show");
return Office.addin
.showAsTaskpane()
.then(function () {
return;
})
.catch(function (error) {
return error.code;
});
const triggerActionLoadLogTest: TriggerAction = {
id: "1",
name: "LoadLogTest",
trigger: {
type: TriggerType.Load,
},
action: {
type: ActionType.LogId,
},
};

const triggerActionExcelNamedRangeTest: TriggerAction = {
id: "2",
name: "NamedRangeTest",
trigger: {
type: TriggerType.ExcelNamedRangeEdit,
parameters: {
namedRangeName: "test",
},
},
action: {
type: ActionType.LogId,
},
};

const triggerActionExcelWorksheetNameRangeAddressTest: TriggerAction = {
id: "3",
name: "Sheet1A1Edit",
trigger: {
type: TriggerType.ExcelWorksheetNameRangeAddressEdit,
parameters: {
worksheetName: "Sheet1",
rangeAddress: "A1",
},
},
action: {
type: ActionType.EvalCallback,
parameters: {
callback: `
async function main(triggerAction) {
console.log(triggerAction.id)
await Excel.run(async (context) => {
const sheet = context.workbook.worksheets.getItemOrNullObject("Sheet1");
const range = sheet.getRange("B1");
range.load("values");
await context.sync();
if (range.isNullObject) {
return;
}
const value = range.values[0][0];
let newValue = 0;
if (typeof value === "number") {
newValue = value + 1;
}
range.values = [[newValue]];
await context.sync();
console.log("Incremented Value");
});
}
`,
},

// type: ActionType.Callback,
// parameters: {
// callback: async (triggerAction: Readonly<TriggerAction>) => {
// // Log that this was called.
// logTriggerId(triggerAction);

// await Excel.run(async (context) => {
// const sheet = context.workbook.worksheets.getItemOrNullObject("Sheet1");
// const range = sheet.getRange("B1");
// range.load("values");
// await context.sync();
// if (range.isNullObject) {
// return;
// }

// const value = range.values[0][0];
// let newValue = 0;
// if (typeof value === "number") {
// newValue = value + 1;
// }

// range.values = [[newValue]];
// await context.sync();
// console.log("Incremented Value");
// });
// },
// },
},
};

/**
* action registry
*/
export const triggerActionsDefault: TriggerAction[] = [
triggerActionLoadLogTest,
triggerActionExcelNamedRangeTest,
triggerActionExcelWorksheetNameRangeAddressTest,
];

// Store triggers in custom XML

type TriggerActionMetadata = Pick<TriggerAction, "id">;

function pruneTriggerActionToTriggerActionMetadata(triggerAction: TriggerAction): TriggerActionMetadata {
const { id } = triggerAction;
return {
id,
};
}

function getTriggerActionJson(triggerAction: TriggerAction): string {
// TODO: prune if needed before storage
const s = JSON.stringify(triggerAction, undefined, 4);
return s;
}

function getTriggerActionFromJson(json: string): TriggerAction {
// TODO: validate the thing read
const triggerAction = JSON.parse(json) as TriggerAction;
return triggerAction;
}

const embedNamespace = "build-add-in-trigger-action";
const embedTag = "TriggerAction";

const storage = getSourceEmbed({
embedNamespace,
embedTag,
pruneItemToItemMetadata: pruneTriggerActionToTriggerActionMetadata,
getItemJson: getTriggerActionJson,
getItemFromJson: getTriggerActionFromJson,
});

Office.actions.associate("HIDETASKPANE", async function () {
console.log("shortcut - Hide");
try {
await Office.addin.hide();
} catch (e) {
console.log(e);
function removeUndefined<T>(array: (T | undefined)[]): T[] {
return array.filter((x) => x !== undefined) as T[];
}

import { embedDeleteById, embedReadAllId } from "./core/embed/embed";
async function removeAllEmbedTriggers() {
const ids = await embedReadAllId({ embedNamespace });
await Promise.all(ids.map((id) => embedDeleteById({ id, embedNamespace })));
}

class StopWatch {
constructor(private name: string) {}
lastStart = 0;
start() {
this.lastStart = Date.now();
}
});

Office.actions.associate("SETCOLOR", function () {
console.log("shortcut - Set Color");
Excel.run(async (context) => {
const range = context.workbook.getSelectedRange();
range.format.fill.load("color");

await context.sync();
const colors = ["#FFFFFF", "#C7CC7A", "#7560BA", "#9DD9D2", "#FFE1A8", "#E26D5C"];
const colorIndex = (colors.indexOf(range.format.fill.color) + 1) % colors.length;
range.format.fill.color = colors[colorIndex];
await context.sync();
});
});
read(header: string = "") {
const current = Date.now();
const delta = current - this.lastStart;
const milliseconds = delta;
console.log(`[${this.name}] ${header} = ${milliseconds} ms`);
}
}

function getWatch(name: string) {
return new StopWatch(name);
}

Office.onReady(() => {
Office.onReady(async ({ host }) => {
console.log("ready");
setHost(host);

const watch = getWatch("boot");
watch.start();

removeAllEmbedTriggers();

// read triggers
let metadata = await storage.getAllItemMetadata();
watch.read("read trigger metadata");

if (metadata.length === 0) {
console.log("save default triggers - start");
// embed some triggers
watch.start();
await Promise.all(triggerActionsDefault.map((item) => storage.saveItem(item)));
metadata = await storage.getAllItemMetadata();
watch.read("save triggers");
console.log("save default triggers - complete");
}

watch.start();
const triggerActions = removeUndefined(
await Promise.all(
metadata.map(({ id }) => {
return storage.getItemById(id);
})
)
);
watch.read("read triggers");

watch.start();
setTriggerActions(triggerActions);
watch.read("setTriggerActions");
registerTriggerActionsInitial();
});
109 changes: 109 additions & 0 deletions src/core/actions/TriggerAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
export enum TriggerType {
/**
* When the shared runtime is loaded
*/
Load = "Load",
/**
* When an excel named range is edited
*/
ExcelNamedRangeEdit = "ExcelNamedRangeEdit",

/**
* When an excel worksheet (by name) range (by address) is edited
*/
ExcelWorksheetNameRangeAddressEdit = "ExcelWorksheetNameRangeAddressEdit",
}

export interface TriggerLoad {
type: TriggerType.Load;
}

export interface TriggerExcelNamedRangeEdit {
type: TriggerType.ExcelNamedRangeEdit;
parameters: {
/**
* global named range
*/
namedRangeName: string;
};
}

export interface TriggerExcelWorksheetNameRangeAddressEdit {
type: TriggerType.ExcelWorksheetNameRangeAddressEdit;
parameters: {
/**
* name of the worksheet.
*/
worksheetName: string;

/**
* address of the range.
*/
rangeAddress: string;
};
}

export type Trigger = TriggerLoad | TriggerExcelNamedRangeEdit | TriggerExcelWorksheetNameRangeAddressEdit;

export enum ActionType {
/**
* Log the id of the trigger;
*/
LogId = "LogId",

/**
* Run a callback.
* note: this is an incredibly powerful action and shouldn't be serialized, but ok to use internally.
*/
Callback = "Callback",

/**
* eval the code and execute main function
* !!!WARNING!!! incredibly not secure only for proof of concept
*/
EvalCallback = "EvalCallback",
}

export interface ActionLogId {
type: ActionType.LogId;
}

export interface ActionCallback {
type: ActionType.Callback;
parameters: {
/**
* Hardcoded callback to call.
* @param triggerAction the trigger action that triggered the callback.
* @returns
*/
callback: (triggerAction: Readonly<TriggerAction>) => Promise<void>;
};
}

export interface ActionEvalCallback {
type: ActionType.EvalCallback;
parameters: {
/**
* Hardcoded string callback to call. eval and call main function with TriggerAction/
* @returns
*/
callback: string;
};
}

export type Action = ActionLogId | ActionCallback | ActionEvalCallback;

export interface TriggerAction {
/**
* Unique id to easily identify the Trigger Action
*/
id: string;

/**
* Easily readable identifier.
*/
name: string;

trigger: Trigger;
action: Action;
}
Loading

0 comments on commit cd472cb

Please sign in to comment.