|
| 1 | +# Flow.ConfluenceSearch |
| 2 | + |
| 3 | +Flow Launcher plugin (C# / .NET 9, WPF) that searches Confluence Cloud via the |
| 4 | +REST API and opens results in the browser. Triggered with the `conf` action |
| 5 | +keyword. |
| 6 | + |
| 7 | +For deeper docs see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), |
| 8 | +[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md), and |
| 9 | +[`docs/QUERY-SYNTAX.md`](docs/QUERY-SYNTAX.md). |
| 10 | + |
| 11 | +## Repository layout |
| 12 | + |
| 13 | +- `Flow.ConfluenceSearch/` — plugin assembly |
| 14 | + - `Main.cs` — entry point, implements `IAsyncPlugin`, `ISettingProvider` |
| 15 | + - `ServiceProvider.cs` — DI registration (`Microsoft.Extensions.DependencyInjection`) |
| 16 | + - `ConfluenceClient/` |
| 17 | + - `ConfluenceSearchClient.cs` — HTTP call to `/wiki/rest/api/search` |
| 18 | + - `ConfluenceQueryBuilder.cs` — parses the user query into CQL |
| 19 | + (for the API call) and into URL parameters (for the "open in browser" |
| 20 | + fallback). Uses the fluent pipeline in `QueryBuilderExtensions.cs`. |
| 21 | + - `ConfluenceSearchPage.cs` — DTOs for the API response |
| 22 | + - `Search/` |
| 23 | + - `Searcher.cs` — orchestrates query → CQL → API call → `Result` list, |
| 24 | + with a 300 ms debounce and per-call timeout |
| 25 | + - `ResultCreator.cs` — builds `Flow.Launcher.Plugin.Result` items, opens |
| 26 | + URLs via `Process.Start(... UseShellExecute=true)` |
| 27 | + - `ResultContext.cs` — data carried on each result |
| 28 | + - `Settings/` — WPF settings panel (`SettingsView.xaml(.cs)`, |
| 29 | + `SettingsViewModel.cs`, `PluginSettings.cs`, `Configurator.cs`) |
| 30 | + - `plugin.json` — Flow Launcher manifest (action keyword `conf`, ID, |
| 31 | + version, icon) |
| 32 | + - `Build-Plugin.ps1` — packages the plugin into a ZIP for manual install |
| 33 | + - `Start.ps1` — local dev helper: stops Flow Launcher, builds, copies the |
| 34 | + output into `%APPDATA%\FlowLauncher\Plugins\Confluence Search-1.0.1`, |
| 35 | + restarts Flow Launcher |
| 36 | +- `Flow.ConfluenceSearch.Test/` — xUnit v3 + Shouldly + NSubstitute tests |
| 37 | + (the only test project referenced by the solution) |
| 38 | +- `Flow.ConfluenceSearch.Tests/` — empty placeholder, **not** part of the |
| 39 | + solution; ignore it |
| 40 | +- `.github/workflows/` |
| 41 | + - `build-action.yml` — PR build (`dotnet publish` win-x64) |
| 42 | + - `publish-action.yml` — release on push to `master`, tags `v<plugin.json |
| 43 | + Version>`, attaches the published ZIP |
| 44 | + |
| 45 | +## Build & test |
| 46 | + |
| 47 | +```powershell |
| 48 | +dotnet restore |
| 49 | +dotnet build Flow.ConfluenceSearch.sln -c Release |
| 50 | +dotnet test Flow.ConfluenceSearch.sln |
| 51 | +``` |
| 52 | + |
| 53 | +For interactive plugin development, run `Flow.ConfluenceSearch\Start.ps1` |
| 54 | +from the project directory — it stops Flow Launcher, rebuilds, copies the |
| 55 | +DLLs into the plugin folder and relaunches. |
| 56 | + |
| 57 | +For producing an installable ZIP, run `Flow.ConfluenceSearch\Build-Plugin.ps1`. |
| 58 | + |
| 59 | +The publish workflow tags releases from the `Version` field in |
| 60 | +`Flow.ConfluenceSearch/plugin.json` — bumping that field on `master` is what |
| 61 | +triggers a new GitHub release. |
| 62 | + |
| 63 | +## Query language (handled by `ConfluenceQueryBuilder`) |
| 64 | + |
| 65 | +The user types a query after `conf`. Tokens are space-separated and |
| 66 | +order-independent: |
| 67 | + |
| 68 | +| Token | Effect | |
| 69 | +|---------------|----------------------------------------------------------------------| |
| 70 | +| `#all` | Ignore default-spaces filter | |
| 71 | +| `#KEY` | Restrict to space `KEY` (repeatable) | |
| 72 | +| `@me` | `contributor = currentUser()` | |
| 73 | +| `@name` | `contributor.fullname ~ name` (repeatable, OR-combined with `@me`) | |
| 74 | +| `+label` | `label IN (...)` (repeatable) | |
| 75 | +| `/` | type=folder | |
| 76 | +| `"` | type=blogpost | |
| 77 | +| `.` | type=page | |
| 78 | +| `*` | All types (skip the default `type IN(page,blogpost)` filter) | |
| 79 | +| anything else | Free-text → `(title ~ "tok*" OR text ~ "tok*")` | |
| 80 | + |
| 81 | +If no `#KEY` is given, `PluginSettings.DefaultSpaces` is applied. CQL is |
| 82 | +always suffixed with `order by lastmodified DESC`. |
| 83 | + |
| 84 | +`ConfluenceQueryBuilderTest.cs` is the canonical reference for expected CQL |
| 85 | +strings — when changing the builder, update or add an `[InlineData]` row |
| 86 | +there. |
| 87 | + |
| 88 | +## Architecture notes |
| 89 | + |
| 90 | +- DI is wired in `ServiceProvider.ConfigureServices`. `PluginInitContext` and |
| 91 | + `PluginSettings` are singletons; everything else is scoped. |
| 92 | +- `HttpClient` is created per call via a `Func<HttpClient>` factory because |
| 93 | + `BaseUrl`, `ApiToken` and `Timeout` come from settings that the user can |
| 94 | + edit at runtime — capturing a single `HttpClient` would freeze stale |
| 95 | + values. |
| 96 | +- Auth is HTTP Basic with the API token base64-encoded in |
| 97 | + `Authorization: Basic`. For Atlassian Cloud the username portion is the |
| 98 | + account email; the plugin currently sends only the token, which works |
| 99 | + because Atlassian also accepts `Bearer`-style tokens in the basic header |
| 100 | + for some endpoints — verify against the user's tenant if 401s appear. |
| 101 | +- `Searcher.QueryAsync` does a 300 ms `Task.Delay` debounce, then runs the |
| 102 | + request under a linked CTS that combines Flow Launcher's cancellation |
| 103 | + token with a per-call timeout (clamped to 3–30 s). |
| 104 | +- `internalsVisibleTo` is set for the test assembly and for Castle/DynamicProxy |
| 105 | + so NSubstitute can mock `internal` interfaces. |
| 106 | + |
| 107 | +## Conventions |
| 108 | + |
| 109 | +- Target framework: `net9.0-windows`, nullable enabled, WPF (`UseWpf`). |
| 110 | +- Public API surface is intentionally small — most types are `internal` and |
| 111 | + exposed to the test project via `InternalsVisibleTo`. |
| 112 | +- Tests use **xUnit v3** (`xunit.v3` package, `TestContext.Current.CancellationToken`), |
| 113 | + Shouldly for assertions, NSubstitute for mocks. Don't introduce other |
| 114 | + frameworks. |
| 115 | +- Code style follows the default .NET conventions; primary constructors are |
| 116 | + used throughout for DI (e.g. `Searcher(...)`, `ResultCreator(...)`). |
| 117 | +- The plugin ID in `plugin.json` (`EE691863-...`) and the one hardcoded in |
| 118 | + `Build-Plugin.ps1` (`BD32A62C-...`) currently differ. The manifest is |
| 119 | + authoritative — the script's value is unused (it isn't written into the |
| 120 | + output) but should be aligned the next time the script is touched. |
| 121 | + |
| 122 | +## Known wrinkles |
| 123 | + |
| 124 | +- The CI publish workflow listens on `master`, but the default branch is |
| 125 | + `main`. Pushes to `main` therefore won't currently trigger a release — |
| 126 | + fix the trigger before depending on it. |
| 127 | +- `Flow.ConfluenceSearch.Tests/` (with an `s`) sits next to the real |
| 128 | + `Flow.ConfluenceSearch.Test/` and contains an empty file. It is not in |
| 129 | + the solution; safe to delete if cleaning up. |
0 commit comments