mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
This commit is contained in:
parent
b13303c202
commit
fe65d4b9a6
7 changed files with 158 additions and 12 deletions
|
|
@ -100,6 +100,7 @@ async function getPlugins() {
|
|||
|
||||
// Read manifest.json to check for additional files (e.g., config schema)
|
||||
const files = ['manifest.json', 'plugin.js', 'icon.svg'];
|
||||
const requiredFiles = [...files];
|
||||
// manifest.json may live at the root or inside src/
|
||||
const manifestCandidates = [
|
||||
path.join(pluginPath, 'manifest.json'),
|
||||
|
|
@ -112,9 +113,16 @@ async function getPlugins() {
|
|||
manifestRead = true;
|
||||
if (manifest.iFrame !== false) {
|
||||
files.push('index.html');
|
||||
requiredFiles.push('index.html');
|
||||
}
|
||||
if (manifest.jsonSchemaCfg) {
|
||||
files.push(manifest.jsonSchemaCfg);
|
||||
requiredFiles.push(manifest.jsonSchemaCfg);
|
||||
}
|
||||
if (manifest.i18n?.languages) {
|
||||
for (const lang of manifest.i18n.languages) {
|
||||
requiredFiles.push(`i18n/${lang}.json`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch {
|
||||
|
|
@ -124,6 +132,7 @@ async function getPlugins() {
|
|||
if (!manifestRead) {
|
||||
// No manifest.json found — assume index.html is needed
|
||||
files.push('index.html');
|
||||
requiredFiles.push('index.html');
|
||||
}
|
||||
|
||||
plugins.push({
|
||||
|
|
@ -131,6 +140,7 @@ async function getPlugins() {
|
|||
path: `packages/plugin-dev/${entry.name}`,
|
||||
buildCommand: hasRealBuildScript ? 'npm run build' : undefined,
|
||||
files,
|
||||
requiredFiles,
|
||||
sourcePath: hasRealBuildScript ? 'dist' : undefined,
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export interface FinishDayPayload {
|
|||
|
||||
export interface LanguageChangePayload {
|
||||
code: string;
|
||||
newLanguage: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
|
@ -472,6 +473,7 @@ export interface PluginAppState {
|
|||
|
||||
export interface PluginAPI {
|
||||
cfg: PluginBaseCfg;
|
||||
readonly Hooks: typeof PluginHooks;
|
||||
|
||||
registerHook<T extends Hooks>(hook: T, fn: PluginHookHandler<T>): void;
|
||||
|
||||
|
|
@ -630,6 +632,16 @@ export interface PluginAPI {
|
|||
|
||||
getConfig<T = Record<string, unknown>>(): Promise<T | null>;
|
||||
|
||||
// i18n
|
||||
translate(key: string, params?: Record<string, string | number>): string;
|
||||
|
||||
formatDate(
|
||||
date: Date | string | number,
|
||||
format: 'short' | 'medium' | 'long' | 'time' | 'datetime',
|
||||
): string;
|
||||
|
||||
getCurrentLanguage(): string;
|
||||
|
||||
// oauth
|
||||
startOAuthFlow(config: OAuthFlowConfig): Promise<OAuthTokenResult>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,59 @@
|
|||
// Procrastination Buster Plugin for Super Productivity
|
||||
import { PluginAPI } from '@super-productivity/plugin-api';
|
||||
import type { PluginAPI } from '@super-productivity/plugin-api';
|
||||
|
||||
declare const plugin: PluginAPI;
|
||||
|
||||
const PLUGIN_ID = 'procrastination-buster';
|
||||
|
||||
type PluginMessage = {
|
||||
type?: unknown;
|
||||
payload?: {
|
||||
key?: unknown;
|
||||
params?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const getPluginMessage = (message: unknown): PluginMessage | undefined =>
|
||||
message && typeof message === 'object' ? (message as PluginMessage) : undefined;
|
||||
|
||||
const isTranslateParams = (params: unknown): params is Record<string, string | number> =>
|
||||
!!params &&
|
||||
typeof params === 'object' &&
|
||||
Object.values(params).every(
|
||||
(value) => typeof value === 'string' || typeof value === 'number',
|
||||
);
|
||||
|
||||
const getLanguageFromHookPayload = (payload: unknown): string | undefined => {
|
||||
if (typeof payload === 'string') {
|
||||
return payload;
|
||||
}
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const languagePayload = payload as {
|
||||
code?: unknown;
|
||||
newLanguage?: unknown;
|
||||
language?: unknown;
|
||||
};
|
||||
|
||||
if (typeof languagePayload.code === 'string') {
|
||||
return languagePayload.code;
|
||||
}
|
||||
if (typeof languagePayload.newLanguage === 'string') {
|
||||
return languagePayload.newLanguage;
|
||||
}
|
||||
if (typeof languagePayload.language === 'string') {
|
||||
return languagePayload.language;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getPluginIframe = (): HTMLIFrameElement | null =>
|
||||
document.querySelector<HTMLIFrameElement>(`iframe[data-plugin-id="${PLUGIN_ID}"]`) ??
|
||||
document.querySelector<HTMLIFrameElement>('iframe[data-plugin-iframe]');
|
||||
|
||||
// Plugin initialization
|
||||
plugin.log.info('Procrastination Buster plugin initialized');
|
||||
|
||||
|
|
@ -11,12 +62,22 @@ plugin.log.info('Procrastination Buster plugin initialized');
|
|||
|
||||
// i18n support - handle translation requests from iframe
|
||||
if (plugin.onMessage) {
|
||||
plugin.onMessage(async (message: any) => {
|
||||
switch (message?.type) {
|
||||
plugin.onMessage((message: unknown) => {
|
||||
const pluginMessage = getPluginMessage(message);
|
||||
|
||||
switch (pluginMessage?.type) {
|
||||
case 'translate':
|
||||
return await plugin.translate(message.payload.key, message.payload.params);
|
||||
if (typeof pluginMessage.payload?.key !== 'string') {
|
||||
return { error: 'Missing translation key' };
|
||||
}
|
||||
return plugin.translate(
|
||||
pluginMessage.payload.key,
|
||||
isTranslateParams(pluginMessage.payload.params)
|
||||
? pluginMessage.payload.params
|
||||
: undefined,
|
||||
);
|
||||
case 'getCurrentLanguage':
|
||||
return await plugin.getCurrentLanguage();
|
||||
return plugin.getCurrentLanguage();
|
||||
default:
|
||||
return { error: 'Unknown message type' };
|
||||
}
|
||||
|
|
@ -24,13 +85,15 @@ if (plugin.onMessage) {
|
|||
}
|
||||
|
||||
// Listen for language changes and notify iframe
|
||||
plugin.registerHook('languageChange', (language: string) => {
|
||||
plugin.registerHook(plugin.Hooks.LANGUAGE_CHANGE, (payload: unknown) => {
|
||||
const language = getLanguageFromHookPayload(payload);
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify the iframe about language change
|
||||
const iframe = document.querySelector('iframe[data-plugin-iframe]');
|
||||
if (iframe && (iframe as HTMLIFrameElement).contentWindow) {
|
||||
(iframe as HTMLIFrameElement).contentWindow!.postMessage(
|
||||
{ type: 'languageChanged', language },
|
||||
'*',
|
||||
);
|
||||
const iframe = getPluginIframe();
|
||||
if (iframe?.contentWindow) {
|
||||
iframe.contentWindow.postMessage({ type: 'languageChanged', language }, '*');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,6 +38,15 @@ function copyRecursive(src, dest) {
|
|||
}
|
||||
}
|
||||
|
||||
function assertFilesExist(basePath, files, pluginName) {
|
||||
const missing = files.filter((file) => !fs.existsSync(path.join(basePath, file)));
|
||||
if (missing.length) {
|
||||
throw new Error(
|
||||
`${pluginName} build is missing required asset(s): ${missing.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin configurations
|
||||
const plugins = [
|
||||
{
|
||||
|
|
@ -47,6 +56,11 @@ const plugins = [
|
|||
copyToAssets: true,
|
||||
buildCommand: async (pluginPath) => {
|
||||
await execAsync(`cd ${pluginPath} && npm run build`);
|
||||
assertFilesExist(
|
||||
path.join(pluginPath, 'dist'),
|
||||
['i18n/en.json', 'i18n/de.json'],
|
||||
'procrastination-buster',
|
||||
);
|
||||
// Copy to assets directory
|
||||
const targetDir = path.join(
|
||||
__dirname,
|
||||
|
|
@ -64,6 +78,11 @@ const plugins = [
|
|||
copyRecursive(src, dest);
|
||||
}
|
||||
}
|
||||
assertFilesExist(
|
||||
targetDir,
|
||||
['i18n/en.json', 'i18n/de.json'],
|
||||
'procrastination-buster',
|
||||
);
|
||||
return 'Built and copied to assets';
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { TaskSharedActions } from '../root-store/meta/task-shared.actions';
|
|||
import { PlannerActions } from '../features/planner/store/planner.actions';
|
||||
import { TaskWithSubTasks } from '../features/tasks/task.model';
|
||||
import { PluginHooks } from './plugin-api.model';
|
||||
import { PluginI18nService } from './plugin-i18n.service';
|
||||
import {
|
||||
selectCurrentTask,
|
||||
selectTaskById,
|
||||
|
|
@ -17,11 +18,15 @@ import { selectPluginUserDataFeatureState } from './store/plugin-user-data.reduc
|
|||
import { SyncTriggerService } from '../imex/sync/sync-trigger.service';
|
||||
import { HydrationStateService } from '../op-log/apply/hydration-state.service';
|
||||
import { PluginUserData } from './plugin-persistence.model';
|
||||
import { updateGlobalConfigSection } from '../features/config/store/global-config.actions';
|
||||
import { selectLocalizationConfig } from '../features/config/store/global-config.reducer';
|
||||
import { LanguageCode } from '../core/locale.constants';
|
||||
|
||||
describe('PluginHooksEffects', () => {
|
||||
let effects: PluginHooksEffects;
|
||||
let actions$: Observable<any>;
|
||||
let pluginServiceMock: jasmine.SpyObj<PluginService>;
|
||||
let pluginI18nServiceMock: jasmine.SpyObj<PluginI18nService>;
|
||||
let store: MockStore;
|
||||
let gateSubject: ReplaySubject<boolean>;
|
||||
|
||||
|
|
@ -64,6 +69,9 @@ describe('PluginHooksEffects', () => {
|
|||
'dispatchHook',
|
||||
'dispatchHookToPlugin',
|
||||
]);
|
||||
pluginI18nServiceMock = jasmine.createSpyObj('PluginI18nService', [
|
||||
'setCurrentLanguage',
|
||||
]);
|
||||
|
||||
// MUST be a ReplaySubject(1) / Subject — NOT BehaviorSubject(true) or
|
||||
// of(true). The boot-suppression spec depends on the gate being un-emitted
|
||||
|
|
@ -87,6 +95,7 @@ describe('PluginHooksEffects', () => {
|
|||
},
|
||||
}),
|
||||
{ provide: PluginService, useValue: pluginServiceMock },
|
||||
{ provide: PluginI18nService, useValue: pluginI18nServiceMock },
|
||||
// workContextChange$ reads activeWorkContext$ at construction; an
|
||||
// empty stream keeps that effect inert for the other effects' tests.
|
||||
{ provide: WorkContextService, useValue: { activeWorkContext$: EMPTY } },
|
||||
|
|
@ -380,6 +389,36 @@ describe('PluginHooksEffects', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('languageChange$', () => {
|
||||
it('updates plugin i18n and dispatches a language hook payload compatible with documented and typed plugins', (done) => {
|
||||
store.overrideSelector(selectLocalizationConfig, {
|
||||
lng: LanguageCode.de,
|
||||
dateTimeLocale: undefined,
|
||||
firstDayOfWeek: undefined,
|
||||
});
|
||||
actions$ = of(
|
||||
updateGlobalConfigSection({
|
||||
sectionKey: 'localization',
|
||||
sectionCfg: {},
|
||||
}),
|
||||
);
|
||||
|
||||
effects.languageChange$.subscribe(() => {
|
||||
expect(pluginI18nServiceMock.setCurrentLanguage).toHaveBeenCalledOnceWith(
|
||||
LanguageCode.de,
|
||||
);
|
||||
expect(pluginServiceMock.dispatchHook).toHaveBeenCalledWith(
|
||||
PluginHooks.LANGUAGE_CHANGE,
|
||||
{
|
||||
code: LanguageCode.de,
|
||||
newLanguage: LanguageCode.de,
|
||||
},
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('firePersistedDataChanged$', () => {
|
||||
const entry = (id: string, data: string): PluginUserData => ({ id, data });
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ export class PluginHooksEffects {
|
|||
|
||||
// Dispatch hook to notify plugins
|
||||
this.pluginService.dispatchHook(PluginHooks.LANGUAGE_CHANGE, {
|
||||
code: newLanguage,
|
||||
newLanguage,
|
||||
});
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@
|
|||
<iframe
|
||||
#iframe
|
||||
[src]="iframeSrc()"
|
||||
data-plugin-iframe
|
||||
[attr.data-plugin-id]="pluginId()"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
|
||||
class="plugin-iframe"
|
||||
[class.hidden]="isLoading()"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue