diff --git a/README.md b/README.md index 6e3bc4776..b49dd7268 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ - The **anti-procrastination feature** helps you gain perspective when you really need to. - Need some extra focus? A **Pomodoro timer** is also always at hand. - **Collect personal metrics** to see, which of your work routines need adjustments. -- Integrate with **Jira**, **GitHub**, **GitLab**, **Gitea** and **OpenProject**. Auto import tasks assigned to you, plan the details locally, automatically create work logs, and get notified immediately, when something changes. +- Integrate with **Jira**, **Trello**, **GitHub**, **GitLab**, **Gitea** and **OpenProject**. Auto import tasks assigned to you, plan the details locally, automatically create work logs, and get notified immediately, when something changes. - Basic [**CalDAV**](https://github.com/johannesjo/super-productivity/blob/master/docs/caldav.md) integration. - Back up and synchronize your data across multiple devices with **Dropbox** and **WebDAV** support - Attach context information to tasks and projects. Create **notes**, attach **files** or create **project-level bookmarks** for links, files, and even commands. diff --git a/src/app/core/theme/global-theme.service.ts b/src/app/core/theme/global-theme.service.ts index 2b7cb23e5..1d177e745 100644 --- a/src/app/core/theme/global-theme.service.ts +++ b/src/app/core/theme/global-theme.service.ts @@ -160,6 +160,8 @@ export class GlobalThemeService { ['repeat', 'assets/icons/repeat.svg'], ['gitea', 'assets/icons/gitea.svg'], ['redmine', 'assets/icons/redmine.svg'], + // trello icon + ['trello', 'assets/icons/trello.svg'], ['calendar', 'assets/icons/calendar.svg'], ['early_on', 'assets/icons/early-on.svg'], ['tomorrow', 'assets/icons/tomorrow.svg'], diff --git a/src/app/features/issue-panel/issue-panel-calendar-agenda/issue-panel-calendar-agenda.component.html b/src/app/features/issue-panel/issue-panel-calendar-agenda/issue-panel-calendar-agenda.component.html index 1a0aebecf..600bf2fc7 100644 --- a/src/app/features/issue-panel/issue-panel-calendar-agenda/issue-panel-calendar-agenda.component.html +++ b/src/app/features/issue-panel/issue-panel-calendar-agenda/issue-panel-calendar-agenda.component.html @@ -16,7 +16,7 @@ [actionAdvice]="'Check your config!'" > } @else if (!agendaItems()?.length) { -
No items found (already added are not shown)
+
No items found.
} @else {
@for (day of agendaItems(); track day.dayStr) { diff --git a/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.html b/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.html index e2dae3a9f..7087603e2 100644 --- a/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.html +++ b/src/app/features/issue-panel/issue-provider-setup-overview/issue-provider-setup-overview.component.html @@ -33,6 +33,13 @@ Jira + + + + @if (isLoading) { + No boards found (yet)... + } + + @for (board of boards$ | async; track board.id) { + + {{ board.name }} + + } + @if ((boards$ | async)?.length === 0 && isCredentialsComplete) { + No boards available + } + @if (!isCredentialsComplete) { + Enter API key and token first + } + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TrelloAdditionalCfgComponent implements OnInit, OnDestroy { + // inject some of the service + private _trelloApiService = inject(TrelloApiService); + private _snackService = inject(SnackService); + private _cdr = inject(ChangeDetectorRef); + + // inputs and outputs + readonly section = input>(); + readonly modelChange = output(); + + // state + private _cfg?: IssueProviderTrello; + private _credentialsChanged$ = new BehaviorSubject(null); + private _boardsList$ = new BehaviorSubject([]); + + selectedBoardId?: string | null; + isCredentialsComplete = false; + + boards$: Observable; + // lets make it true at first (since we load data when user first change their board) + isLoading$ = new BehaviorSubject(true); + + private _subs = new Subscription(); + + constructor() { + // Initialize boards$ with proper debounce and switchMap + this.boards$ = this._credentialsChanged$.pipe( + debounceTime(1000), // Wait 1 seconds after user stops typing + switchMap((cfg) => { + // Check if we have minimum required credentials (apiKey and token) + if (!cfg || !cfg.apiKey || !cfg.token) { + this.isCredentialsComplete = false; + this.isLoading$.next(false); + this._boardsList$.next([]); + this._cdr.markForCheck(); + return of([]); + } + + this.isCredentialsComplete = true; + this.isLoading$.next(true); + this._cdr.markForCheck(); + + // Create a temporary config with a placeholder boardId for the API call + const tempCfgForFetch = { ...cfg, boardId: 'temp' }; + + // Fetch all boards from user + return this._trelloApiService.getBoards$(tempCfgForFetch).pipe( + map((response) => { + // Map Trello API response to our format + const boards = (response || []).map((board: any) => ({ + id: board.id, + name: board.name, + })); + // Store boards in BehaviorSubject so we can access them later + this._boardsList$.next(boards); + return boards; + }), + tap(() => { + this.isLoading$.next(false); + this._cdr.markForCheck(); + }), + catchError((error) => { + this.isLoading$.next(false); + this._boardsList$.next([]); + this._cdr.markForCheck(); + // Show error notification + this._snackService.open({ + type: 'ERROR', + msg: 'Failed to load Trello boards. Check your API credentials.', + isSkipTranslate: true, + }); + return of([]); + }), + ); + }), + startWith([]), + ); + } + + @Input() set cfg(cfg: IssueProviderTrello) { + this._cfg = cfg; + this.selectedBoardId = cfg.boardId; + // Emit credential change to trigger debounced API call + // Only emit if apiKey or token is present (boardId not required for fetching boards list) + if (cfg.apiKey || cfg.token) { + this._credentialsChanged$.next(cfg); + } + } + + ngOnInit(): void { + // Load boards if credentials already exist + if (this._cfg && (this._cfg.apiKey || this._cfg.token)) { + this._credentialsChanged$.next(this._cfg); + } + } + + ngOnDestroy(): void { + this._subs.unsubscribe(); + this._credentialsChanged$.complete(); + this._boardsList$.complete(); + } + + onBoardSelect(boardId: string | null): void { + if (this._cfg && boardId) { + // Get the board name from the stored boards list + const selectedBoard = this._boardsList$.value.find((board) => board.id === boardId); + + const updated: IssueProviderTrello = { + ...this._cfg, + boardId, + boardName: selectedBoard?.name || null, // Add board name + }; + this._cfg = updated; + this.modelChange.emit(updated); + } + } + + reloadBoards(): void { + if (!this._cfg || !this._cfg.apiKey || !this._cfg.token) { + this._snackService.open({ + type: 'ERROR', + msg: 'Enter API key and token first.', + isSkipTranslate: true, + }); + return; + } + + this.isCredentialsComplete = true; + this.isLoading$.next(true); + this._credentialsChanged$.next({ ...this._cfg }); + this._cdr.markForCheck(); + } +} diff --git a/src/app/features/issue/providers/trello/trello.const.ts b/src/app/features/issue/providers/trello/trello.const.ts new file mode 100644 index 000000000..a7cdba15b --- /dev/null +++ b/src/app/features/issue/providers/trello/trello.const.ts @@ -0,0 +1,80 @@ +// Necessary fields for trello configuration. Used for building the form, alongside with several essential properties. + +import { + ConfigFormSection, + LimitedFormlyFieldConfig, +} from '../../../config/global-config.model'; +import { ISSUE_PROVIDER_COMMON_FORM_FIELDS } from '../../common-issue-form-stuff.const'; +import { IssueProviderTrello } from '../../issue.model'; +import { TrelloCfg } from './trello.model'; + +export const DEFAULT_TRELLO_CFG: TrelloCfg = { + isEnabled: false, + apiKey: null, + token: null, + boardId: null, +}; + +export const TRELLO_POLL_INTERVAL = 5 * 60 * 1000; +export const TRELLO_CONFIG_FORM: LimitedFormlyFieldConfig[] = [ + // ...CROSS_ORIGIN_WARNING, + // TODO: add instruction for https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/ + { + key: 'apiKey', + type: 'input', + props: { + label: 'Trello API key', + description: 'Insert the Trello API key here', + type: 'text', + required: true, + }, + }, + { + key: 'token', + type: 'input', + props: { + label: 'Trello API token', + description: 'Insert the Trello API token here.', + type: 'password', + required: true, + }, + validators: { + token: { + expression: (c: { value: string | undefined }) => + !c.value || c.value.length >= 32, + message: 'Trello token is typically 32+ characters', + }, + }, + }, + // search boards + { + type: 'collapsible', + props: { label: 'Advanced Config' }, + fieldGroup: [...ISSUE_PROVIDER_COMMON_FORM_FIELDS], + }, +]; + +export const TRELLO_CONFIG_FORM_SECTION: ConfigFormSection = { + title: 'Trello', + key: 'TRELLO', + items: TRELLO_CONFIG_FORM, + helpArr: [ + { + h: 'Getting Started', + p: 'To connect Super Productivity with Trello, you need to generate an API key and token from your Trello account. This allows Super Productivity to access your boards and cards.', + }, + { + h: 'How to Get API Key & Token', + p: 'Visit https://trello.com/power-ups/admin and create a new app. Fills in necessary detail excluding icon. After creating the app, click on "Generate a new API key". This will allow you to view your API key. Token can be generated upon clicking Token hyperlink in the API key section and you can copy it after you have done reviewing your application. You will need both the key and token to configure the integration. See https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/ for more detail if you are unsure of what to do.', + p2: 'The token grants Super Productivity permission to read your Trello data. You can revoke it at any time from the Trello security page.', + }, + { + h: 'Selecting Your Board', + p: 'After entering your API key and token, click "Load Trello Boards" and you will be able to select the Trello board you want to work with. Only cards from the selected board will be accessible in Super Productivity.', + }, + { + h: 'Features', + p: 'Once configured, you can search for Trello cards, add them as tasks, view card details including attachments, and keep your task data in sync with Trello.', + }, + ], +}; diff --git a/src/app/features/issue/providers/trello/trello.model.ts b/src/app/features/issue/providers/trello/trello.model.ts new file mode 100644 index 000000000..8e66142c4 --- /dev/null +++ b/src/app/features/issue/providers/trello/trello.model.ts @@ -0,0 +1,14 @@ +/** + * Configuration model for the Trello integration. + */ + +import { BaseIssueProviderCfg } from '../../issue.model'; +export interface TrelloCfg extends BaseIssueProviderCfg { + isEnabled: boolean; + apiKey: string | null; + token: string | null; + boardId: string | null; + + // experimental: board - add board name + boardName?: string | null; +} diff --git a/src/assets/icons/trello.svg b/src/assets/icons/trello.svg new file mode 100644 index 000000000..f8f4ec1cc --- /dev/null +++ b/src/assets/icons/trello.svg @@ -0,0 +1,5 @@ + + + + +