Merge pull request #5459 from Mustache-Games/conflicts-resolved

feat(plugins): add simple counter API methods for plugins
This commit is contained in:
Johannes Millan 2025-11-17 12:28:58 +01:00 committed by GitHub
commit 2706c73db2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 344 additions and 133 deletions

View file

@ -260,7 +260,7 @@ export class PluginAPI implements PluginAPIInterface {
return this._boundMethods.loadPersistedData();
}
async getConfig<T>(): Promise<T> {
async getConfig(): Promise<any> {
PluginLog.log(`Plugin ${this._pluginId} requested configuration`);
return this._boundMethods.getConfig();
}
@ -324,7 +324,27 @@ export class PluginAPI implements PluginAPIInterface {
}
/**
* Set a simple counter value for today
* Gets all simple counters as { [key: string]: number }.
*/
async getAllCounters(): Promise<{ [key: string]: number }> {
PluginLog.log(`Plugin ${this._pluginId} requested all simple counters`);
return this._pluginBridge.getAllCounters();
}
/**
* Gets a single simple counter value (undefined if unset).
* @param id The counter id (e.g., 'daily-commits').
*/
async getCounter(id: string): Promise<number | null> {
PluginLog.log(`Plugin ${this._pluginId} requested counter value for id: ${id}`);
const value = await this._pluginBridge.getCounter(id);
return value ?? null;
}
/**
* Sets a simple counter value.
* @param id The counter id.
* @param value The numeric value.
*/
async setCounter(id: string, value: number): Promise<void> {
PluginLog.log(`Plugin ${this._pluginId} requested to set counter ${id} to ${value}`);
@ -332,47 +352,123 @@ export class PluginAPI implements PluginAPIInterface {
}
/**
* Get a simple counter value for today
* Increments a simple counter (default +1).
* @param id The counter id.
* @param incrementBy Increment amount (default: 1).
*/
async getCounter(id: string): Promise<number | null> {
PluginLog.log(`Plugin ${this._pluginId} requested to get counter ${id}`);
return this._pluginBridge.getCounter(id);
}
/**
* Increment a simple counter value for today
*/
async incrementCounter(id: string, incrementBy: number = 1): Promise<number> {
async incrementCounter(id: string, incrementBy = 1): Promise<number> {
PluginLog.log(
`Plugin ${this._pluginId} requested to increment counter ${id} by ${incrementBy}`,
);
return this._pluginBridge.incrementCounter(id, incrementBy);
const newValue = await this._pluginBridge.incrementCounter(id, incrementBy);
return newValue;
}
/**
* Decrement a simple counter value for today
* Decrements a simple counter (floors at 0, default -1).
* @param id The counter id.
* @param decrementBy Decrement amount (default: 1).
*/
async decrementCounter(id: string, decrementBy: number = 1): Promise<number> {
async decrementCounter(id: string, decrementBy = 1): Promise<number> {
PluginLog.log(
`Plugin ${this._pluginId} requested to decrement counter ${id} by ${decrementBy}`,
);
return this._pluginBridge.decrementCounter(id, decrementBy);
const newValue = await this._pluginBridge.decrementCounter(id, decrementBy);
return newValue;
}
/**
* Delete a simple counter
* Deletes a simple counter.
* @param id The counter ID.
*/
async deleteCounter(id: string): Promise<void> {
PluginLog.log(`Plugin ${this._pluginId} requested to delete counter ${id}`);
return this._pluginBridge.deleteCounter(id);
return this._pluginBridge.deleteSimpleCounter(id);
}
/**
* Get all simple counters with their values for today
* Gets all simple counters as SimpleCounter[].
*/
async getAllCounters(): Promise<{ [id: string]: number }> {
PluginLog.log(`Plugin ${this._pluginId} requested to get all counters`);
return this._pluginBridge.getAllCounters();
async getAllSimpleCounters(): Promise<any[]> {
PluginLog.log(`Plugin ${this._pluginId} requested all simple counters (full model)`);
return this._pluginBridge.getAllSimpleCounters();
}
/**
* Gets a single simple counter by ID.
* @param id The counter ID.
*/
async getSimpleCounter(id: string): Promise<any | undefined> {
PluginLog.log(`Plugin ${this._pluginId} requested simple counter ${id}`);
return this._pluginBridge.getSimpleCounter(id);
}
/**
* Updates a simple counter (partial).
* @param id The counter ID.
* @param updates Partial updates.
*/
async updateSimpleCounter(id: string, updates: Partial<any>): Promise<void> {
PluginLog.log(
`Plugin ${this._pluginId} requested to update simple counter ${id}`,
updates,
);
return this._pluginBridge.updateSimpleCounter(id, updates);
}
/**
* Toggles a simple counter's isOn state.
* @param id The counter ID.
*/
async toggleSimpleCounter(id: string): Promise<void> {
PluginLog.log(`Plugin ${this._pluginId} requested to toggle simple counter ${id}`);
return this._pluginBridge.toggleSimpleCounter(id);
}
/**
* Sets a simple counter's isEnabled state.
* @param id The counter ID.
* @param isEnabled Enabled state.
*/
async setSimpleCounterEnabled(id: string, isEnabled: boolean): Promise<void> {
PluginLog.log(
`Plugin ${this._pluginId} requested to set simple counter ${id} enabled: ${isEnabled}`,
);
return this._pluginBridge.setSimpleCounterEnabled(id, isEnabled);
}
/**
* Deletes a simple counter.
* @param id The counter ID.
*/
async deleteSimpleCounter(id: string): Promise<void> {
PluginLog.log(`Plugin ${this._pluginId} requested to delete simple counter ${id}`);
return this._pluginBridge.deleteSimpleCounter(id);
}
/**
* Sets a simple counter value for today.
* @param id The counter ID.
* @param value The numeric value.
*/
async setSimpleCounterToday(id: string, value: number): Promise<void> {
PluginLog.log(
`Plugin ${this._pluginId} requested to set simple counter ${id} today to ${value}`,
);
return this._pluginBridge.setSimpleCounterToday(id, value);
}
/**
* Sets a simple counter value for a specific date.
* @param id The counter ID.
* @param date The date (`YYYY-MM-DD`).
* @param value The numeric value.
*/
async setSimpleCounterDate(id: string, date: string, value: number): Promise<void> {
PluginLog.log(
`Plugin ${this._pluginId} requested to set simple counter ${id} on ${date} to ${value}`,
);
return this._pluginBridge.setSimpleCounterDate(id, date, value);
}
/**

View file

@ -34,7 +34,7 @@ import { WorkContextService } from '../features/work-context/work-context.servic
import { ProjectService } from '../features/project/project.service';
import { TagService } from '../features/tag/tag.service';
import typia from 'typia';
import { first } from 'rxjs/operators';
import { first, take, map } from 'rxjs/operators';
import { selectTaskByIdWithSubTaskData } from '../features/tasks/store/task.selectors';
import { PluginUserPersistenceService } from './plugin-user-persistence.service';
import { PluginConfigService } from './plugin-config.service';
@ -50,7 +50,16 @@ import { Log, PluginLog } from '../core/log';
import { TaskCopy } from '../features/tasks/task.model';
import { ProjectCopy } from '../features/project/project.model';
import { TagCopy } from '../features/tag/tag.model';
import { SimpleCounterService } from '../features/simple-counter/simple-counter.service';
// New imports for simple counters
import { selectAllSimpleCounters } from '../features/simple-counter/store/simple-counter.reducer';
import { SimpleCounter } from '../features/simple-counter/simple-counter.model';
import {
upsertSimpleCounter,
updateSimpleCounter,
deleteSimpleCounter,
toggleSimpleCounterCounter,
} from '../features/simple-counter/store/simple-counter.actions';
/**
* PluginBridge acts as an intermediary layer between plugins and the main application services.
@ -80,7 +89,6 @@ export class PluginBridgeService implements OnDestroy {
private _injector = inject(Injector);
private _translateService = inject(TranslateService);
private _syncWrapperService = inject(SyncWrapperService);
private _simpleCounterService = inject(SimpleCounterService);
// Track header buttons registered by plugins
private readonly _headerButtons = signal<PluginHeaderBtnCfg[]>([]);
@ -112,7 +120,7 @@ export class PluginBridgeService implements OnDestroy {
): {
persistDataSynced: (dataStr: string) => Promise<void>;
loadPersistedData: () => Promise<string | null>;
getConfig: <T>() => Promise<T>;
getConfig: () => Promise<any>;
registerHeaderButton: (cfg: PluginHeaderBtnCfg) => void;
registerMenuEntry: (cfg: Omit<PluginMenuEntryCfg, 'pluginId'>) => void;
registerSidePanelButton: (cfg: Omit<PluginSidePanelBtnCfg, 'pluginId'>) => void;
@ -123,6 +131,19 @@ export class PluginBridgeService implements OnDestroy {
executeNodeScript: (
request: PluginNodeScriptRequest,
) => Promise<PluginNodeScriptResult>;
getAllCounters: () => Promise<{ [key: string]: number }>;
getCounter: (id: string) => Promise<number | null>;
setCounter: (id: string, value: number) => Promise<void>;
incrementCounter: (id: string, incrementBy?: number) => Promise<number>;
decrementCounter: (id: string, decrementBy?: number) => Promise<number>;
getAllSimpleCounters: () => Promise<SimpleCounter[]>;
getSimpleCounter: (id: string) => Promise<SimpleCounter | undefined>;
updateSimpleCounter: (id: string, updates: Partial<SimpleCounter>) => Promise<void>;
toggleSimpleCounter: (id: string) => Promise<void>;
setSimpleCounterEnabled: (id: string, isEnabled: boolean) => Promise<void>;
deleteSimpleCounter: (id: string) => Promise<void>;
setSimpleCounterToday: (id: string, value: number) => Promise<void>;
setSimpleCounterDate: (id: string, date: string, value: number) => Promise<void>;
log: ReturnType<typeof Log.withContext>;
} {
return {
@ -154,6 +175,29 @@ export class PluginBridgeService implements OnDestroy {
executeNodeScript: (request: PluginNodeScriptRequest) =>
this._executeNodeScript(pluginId, manifest || null, request),
// Basic counter methods (existing)
getAllCounters: () => this.getAllCounters(),
getCounter: (id: string) => this.getCounter(id),
setCounter: (id: string, value: number) => this.setCounter(id, value),
incrementCounter: (id: string, incrementBy = 1) =>
this.incrementCounter(id, incrementBy),
decrementCounter: (id: string, decrementBy = 1) =>
this.decrementCounter(id, decrementBy),
// Full SimpleCounter methods (new)
getAllSimpleCounters: () => this.getAllSimpleCounters(),
getSimpleCounter: (id: string) => this.getSimpleCounter(id),
updateSimpleCounter: (id: string, updates: Partial<SimpleCounter>) =>
this.updateSimpleCounter(id, updates),
toggleSimpleCounter: (id: string) => this.toggleSimpleCounter(id),
setSimpleCounterEnabled: (id: string, isEnabled: boolean) =>
this.setSimpleCounterEnabled(id, isEnabled),
deleteSimpleCounter: (id: string) => this.deleteSimpleCounter(id),
setSimpleCounterToday: (id: string, value: number) =>
this.setSimpleCounterToday(id, value),
setSimpleCounterDate: (id: string, date: string, value: number) =>
this.setSimpleCounterDate(id, date, value),
// Logging
log: Log.withContext(`${pluginId}`),
};
@ -448,113 +492,6 @@ export class PluginBridgeService implements OnDestroy {
PluginLog.log('PluginBridge: Tag updated successfully', { tagId, updates });
}
/**
* Set a simple counter value for today
*/
async setCounter(id: string, value: number): Promise<void> {
typia.assert<string>(id);
typia.assert<number>(value);
this._simpleCounterService.setCounterToday(id, value);
PluginLog.log('PluginBridge: Counter set successfully', { id, value });
}
/**
* Get a simple counter value for today
*/
async getCounter(id: string): Promise<number | null> {
typia.assert<string>(id);
const counter = await this._simpleCounterService.simpleCounters$
.pipe(first())
.toPromise();
const simpleCounter = counter?.find((c) => c.id === id);
if (!simpleCounter) {
return null;
}
const dateService = this._injector.get(
await import('../core/date/date.service').then((m) => m.DateService),
);
const today = dateService.todayStr();
return simpleCounter.countOnDay[today] || 0;
}
/**
* Increment a simple counter value for today
*/
async incrementCounter(id: string, incrementBy: number = 1): Promise<number> {
typia.assert<string>(id);
typia.assert<number>(incrementBy);
this._simpleCounterService.increaseCounterToday(id, incrementBy);
// Get the new value
const newValue = await this.getCounter(id);
PluginLog.log('PluginBridge: Counter incremented successfully', {
id,
incrementBy,
newValue,
});
return newValue ?? incrementBy;
}
/**
* Decrement a simple counter value for today
*/
async decrementCounter(id: string, decrementBy: number = 1): Promise<number> {
typia.assert<string>(id);
typia.assert<number>(decrementBy);
this._simpleCounterService.decreaseCounterToday(id, decrementBy);
// Get the new value
const newValue = await this.getCounter(id);
PluginLog.log('PluginBridge: Counter decremented successfully', {
id,
decrementBy,
newValue,
});
return newValue ?? -decrementBy;
}
/**
* Delete a simple counter
*/
async deleteCounter(id: string): Promise<void> {
typia.assert<string>(id);
this._simpleCounterService.deleteSimpleCounter(id);
PluginLog.log('PluginBridge: Counter deleted successfully', { id });
}
/**
* Get all simple counters with their values for today
*/
async getAllCounters(): Promise<{ [id: string]: number }> {
const counters = await this._simpleCounterService.simpleCounters$
.pipe(first())
.toPromise();
if (!counters) {
return {};
}
const dateService = this._injector.get(
await import('../core/date/date.service').then((m) => m.DateService),
);
const today = dateService.todayStr();
const result: { [id: string]: number } = {};
counters.forEach((counter) => {
result[counter.id] = counter.countOnDay[today] || 0;
});
PluginLog.log('PluginBridge: Retrieved all counters', result);
return result;
}
/**
* Reorder tasks in a project or parent task
* @param taskIds - Array of task IDs in the new order
@ -1211,6 +1148,184 @@ export class PluginBridgeService implements OnDestroy {
handler(this._isWindowFocused);
}
/**
* Gets all simple counters as { [key: string]: number }.
*/
async getAllCounters(): Promise<{ [key: string]: number }> {
const today = new Date().toISOString().split('T')[0];
const countersArray = await this._store
.select(selectAllSimpleCounters)
.pipe(
take(1),
map((counters: SimpleCounter[]) =>
counters.reduce(
(acc, c) => ({ ...acc, [c.id]: c.countOnDay?.[today] ?? 0 }),
{} as { [key: string]: number },
),
),
)
.toPromise();
return countersArray || {};
}
/**
* Gets a single simple counter value (undefined if unset).
* @param key The counter key (e.g., 'daily-commits').
*/
async getCounter(id: string): Promise<number | null> {
typia.assert<string>(id);
// Allow uppercase, lowercase, numbers, hyphens, underscores
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
throw new Error('Invalid counter key: must be alphanumeric with hyphens');
}
const counters = await this.getAllCounters();
return counters[id] ?? null;
}
async setCounter(id: string, value: number): Promise<void> {
typia.assert<string>(id);
typia.assert<number>(value);
// Allow uppercase, lowercase, numbers, hyphens, underscores
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
throw new Error('Invalid counter key: must be alphanumeric with hyphens');
}
if (typeof value !== 'number' || !isFinite(value) || value < 0) {
throw new Error('Invalid counter value: must be a non-negative number');
}
const today = new Date().toISOString().split('T')[0];
// Upsert the counter (creates if not exists)
this._store.dispatch(
upsertSimpleCounter({
simpleCounter: {
id: id,
//title: id,
//isEnabled: true,
//icon: null,
//type: 'ClickCounter',
countOnDay: { [today]: value },
//isOn: false,
} as SimpleCounter,
}),
);
}
async incrementCounter(id: string, incrementBy = 1): Promise<number> {
typia.assert<string>(id);
typia.assert<number>(incrementBy);
if (typeof incrementBy !== 'number' || !isFinite(incrementBy) || incrementBy <= 0) {
throw new Error('Invalid increment amount: must be a positive number');
}
const current = (await this.getCounter(id)) ?? 0;
const newValue = current + incrementBy;
await this.setCounter(id, newValue);
return newValue;
}
async decrementCounter(id: string, decrementBy = 1): Promise<number> {
typia.assert<string>(id);
typia.assert<number>(decrementBy);
if (typeof decrementBy !== 'number' || !isFinite(decrementBy) || decrementBy <= 0) {
throw new Error('Invalid decrement amount: must be a positive number');
}
const current = (await this.getCounter(id)) ?? 0;
const newValue = Math.max(0, current - decrementBy);
await this.setCounter(id, Math.max(0, newValue));
return newValue;
}
/**
* Gets all simple counters as SimpleCounter[].
*/
async getAllSimpleCounters(): Promise<SimpleCounter[]> {
return this._store.select(selectAllSimpleCounters).pipe(take(1)).toPromise();
}
/**
* Gets a single simple counter by ID.
* @param id The counter ID.
*/
async getSimpleCounter(id: string): Promise<SimpleCounter | undefined> {
const all = await this.getAllSimpleCounters();
return all.find((c) => c.id === id);
}
/**
* Updates a simple counter (partial).
* @param id The counter ID.
* @param updates Partial updates.
*/
async updateSimpleCounter(id: string, updates: Partial<SimpleCounter>): Promise<void> {
this._store.dispatch(
updateSimpleCounter({
simpleCounter: { id, changes: updates },
}),
);
}
/**
* Toggles a simple counter's isOn state.
* @param id The counter ID.
*/
async toggleSimpleCounter(id: string): Promise<void> {
const counter = await this.getSimpleCounter(id);
if (!counter) {
throw new Error(`Counter ${id} not found`);
}
this._store.dispatch(toggleSimpleCounterCounter({ id }));
}
/**
* Sets a simple counter's isEnabled state.
* @param id The counter ID.
* @param isEnabled Enabled state.
*/
async setSimpleCounterEnabled(id: string, isEnabled: boolean): Promise<void> {
return this.updateSimpleCounter(id, { isEnabled });
}
/**
* Deletes a simple counter.
* @param id The counter ID.
*/
async deleteSimpleCounter(id: string): Promise<void> {
this._store.dispatch(deleteSimpleCounter({ id }));
}
/**
* Sets a simple counter value for today.
* @param id The counter ID.
* @param value The numeric value.
*/
async setSimpleCounterToday(id: string, value: number): Promise<void> {
const today = new Date().toISOString().split('T')[0];
return this.setSimpleCounterDate(id, today, value);
}
/**
* Sets a simple counter value for a specific date.
* @param id The counter ID.
* @param date The date (`YYYY-MM-DD`).
* @param value The numeric value.
*/
async setSimpleCounterDate(id: string, date: string, value: number): Promise<void> {
if (!date.match(/^\d{4}-\d{2}-\d{2}$/)) {
throw new Error('Invalid date format: use YYYY-MM-DD');
}
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
throw new Error('Invalid date: must be valid YYYY-MM-DD');
}
const counter = await this.getSimpleCounter(id);
if (!counter) {
throw new Error(`Counter ${id} not found`);
}
if (typeof value !== 'number' || !isFinite(value) || value < 0) {
throw new Error('Invalid counter value: must be a non-negative number');
}
const newCountOnDay = { ...counter.countOnDay, [date]: value };
return this.updateSimpleCounter(id, { countOnDay: newCountOnDay });
}
/**
* Clean up all resources when service is destroyed
*/