Skip to content

Commit 347f4a9

Browse files
authored
Merge pull request #8 from haugjan/docs/contributor-restructure
Add contributor docs and ignore .claude/settings.local.json
2 parents a7db951 + d537a13 commit 347f4a9

6 files changed

Lines changed: 427 additions & 0 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,6 @@ $RECYCLE.BIN/
482482

483483
# Vim temporary swap files
484484
*.swp
485+
486+
# Claude Code per-machine local settings
487+
.claude/settings.local.json

CLAUDE.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ dotnet test
173173
4. Add tests if applicable.
174174
5. Submit a pull request.
175175

176+
For contributors there is additional documentation under [`docs/`](docs/):
177+
178+
- [Architecture](docs/ARCHITECTURE.md) — solution layout, request lifecycle, DI setup.
179+
- [Development](docs/DEVELOPMENT.md) — build/test commands, hot-swap dev loop, CI/release flow.
180+
- [Query syntax](docs/QUERY-SYNTAX.md) — full reference of the `conf` query language.
181+
176182
## License
177183

178184
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

docs/ARCHITECTURE.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Architecture
2+
3+
Flow.ConfluenceSearch is a [Flow Launcher](https://flowlauncher.com/) plugin
4+
written in C# / .NET 9 (WPF). It searches Confluence Cloud through the REST
5+
API and is invoked with the `conf` action keyword.
6+
7+
## Solution layout
8+
9+
```
10+
Flow.ConfluenceSearch.sln
11+
├── Flow.ConfluenceSearch/ Plugin assembly
12+
│ ├── Main.cs Entry point (IAsyncPlugin, ISettingProvider)
13+
│ ├── ServiceProvider.cs DI container wiring
14+
│ ├── plugin.json Flow Launcher manifest
15+
│ ├── ConfluenceClient/ HTTP + CQL builder
16+
│ │ ├── ConfluenceSearchClient.cs
17+
│ │ ├── ConfluenceQueryBuilder.cs
18+
│ │ ├── QueryBuilderExtensions.cs
19+
│ │ └── ConfluenceSearchPage.cs Response DTOs
20+
│ ├── Search/
21+
│ │ ├── Searcher.cs Orchestrates query → CQL → results
22+
│ │ ├── ResultCreator.cs Builds Flow Launcher Result items
23+
│ │ └── ResultContext.cs
24+
│ ├── Settings/ WPF settings panel
25+
│ │ ├── SettingsView.xaml(.cs)
26+
│ │ ├── SettingsViewModel.cs
27+
│ │ ├── PluginSettings.cs
28+
│ │ └── Configurator.cs
29+
│ ├── Build-Plugin.ps1 Packages plugin into a ZIP
30+
│ └── Start.ps1 Local hot-swap dev script
31+
└── Flow.ConfluenceSearch.Test/ xUnit v3 + Shouldly + NSubstitute
32+
```
33+
34+
> The folder `Flow.ConfluenceSearch.Tests/` (with an `s`) sits next to the
35+
> real test project, contains an empty file, and is **not** referenced by the
36+
> solution. It can be removed.
37+
38+
## Request lifecycle
39+
40+
1. Flow Launcher invokes `Main.QueryAsync(query, ct)`.
41+
2. `Searcher.QueryAsync`
42+
- waits 300 ms (debounce — typing further cancels via `TaskCanceledException`)
43+
- links the caller's cancellation token with a per-call timeout
44+
(`PluginSettings.Timeout`, clamped to 3–30 s)
45+
- if the query is empty, returns the static "hint" results
46+
3. `ConfluenceQueryBuilder.BuildTextCql` parses the user input into CQL.
47+
`BuildQueryForOpenInBrowser` produces a fallback URL for the
48+
"open search in browser" action.
49+
4. `ConfluenceSearchClient.SearchCqlAsync` calls
50+
`GET /wiki/rest/api/search?cql=…&limit=…&expand=content.history.lastUpdated`
51+
and deserializes `ContentSearchResponse`.
52+
5. `ResultCreator.CreateResult` wraps each hit as a Flow Launcher `Result`.
53+
Activating a result opens its URL via `Process.Start(... UseShellExecute=true)`.
54+
55+
## Dependency injection
56+
57+
`ServiceProvider.ConfigureServices` (called from `Main.InitAsync`) registers:
58+
59+
| Lifetime | Registration |
60+
|-----------|--------------|
61+
| Singleton | `PluginInitContext`, `PluginSettings` |
62+
| Singleton | `Func<HttpClient>` factory (see below) |
63+
| Scoped | `IConfluenceSearchClient`, `IConfluenceQueryBuilder`, `IResultCreator`, `IConfigurator`, `ISearcher`, `SettingsViewModel` |
64+
65+
### Why a `Func<HttpClient>`?
66+
67+
`BaseUrl`, `ApiToken`, and `Timeout` come from settings the user can edit at
68+
runtime. A captured singleton `HttpClient` would freeze stale values, so the
69+
factory creates a fresh client per call, configured with the current
70+
settings. Each created client is `using`-disposed after the request.
71+
72+
Authentication uses HTTP Basic with the API token base64-encoded in the
73+
`Authorization` header.
74+
75+
## Visibility & testability
76+
77+
Most types in the plugin assembly are `internal`. The csproj exposes them to
78+
the test assembly and to NSubstitute via `InternalsVisibleTo`:
79+
80+
- `Flow.ConfluenceSearch.Test`
81+
- `Castle.Core`
82+
- `DynamicProxyGenAssembly2`
83+
84+
This lets tests substitute `internal` interfaces (e.g. `IConfluenceSearchClient`)
85+
without making them public.
86+
87+
## Settings
88+
89+
`PluginSettings` is loaded via Flow Launcher's `LoadSettingJsonStorage<T>()`
90+
and contains:
91+
92+
| Field | Default |
93+
|------------------|---------------------------|
94+
| `BaseUrl` | `"https://www.example.com"` |
95+
| `ApiToken` | `""` |
96+
| `Timeout` | `00:00:10` |
97+
| `MaxResults` | `10` |
98+
| `DefaultSpaces` | `[]` |
99+
100+
The settings UI (`SettingsView.xaml`) binds directly to `Settings.*`. The
101+
API token uses a `PasswordBox` whose `PasswordChanged` handler writes to
102+
`Settings.ApiToken` in the code-behind (passwords cannot be two-way-bound
103+
in WPF without unsafe workarounds).

docs/DEVELOPMENT.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Development
2+
3+
## Prerequisites
4+
5+
- Windows 10/11
6+
- .NET SDK 9.0
7+
- Flow Launcher (only required for end-to-end testing)
8+
9+
## Build & test
10+
11+
```powershell
12+
dotnet restore
13+
dotnet build Flow.ConfluenceSearch.sln -c Release
14+
dotnet test Flow.ConfluenceSearch.sln
15+
```
16+
17+
The solution targets `net9.0-windows`, has nullable reference types enabled,
18+
and uses WPF (`UseWpf=true`).
19+
20+
## Test stack
21+
22+
Tests live in `Flow.ConfluenceSearch.Test/`. The stack:
23+
24+
- **xUnit v3** (`xunit.v3`) — note that v3 uses
25+
`TestContext.Current.CancellationToken` inside `[Theory]`/`[Fact]` methods
26+
rather than injecting a `CancellationToken` parameter.
27+
- **Shouldly** for assertions (`x.ShouldBe(...)`, `Should.ThrowAsync<...>`).
28+
- **NSubstitute** for mocks. Internal interfaces are mockable thanks to the
29+
`InternalsVisibleTo` attributes on the production project.
30+
31+
When changing `ConfluenceQueryBuilder`, add or update `[InlineData]` rows on
32+
`BuildTextCqlTest` / `BuildQueryForOpenInBrowserTest` in
33+
`ConfluenceQueryBuilderTest.cs`. Those tables are the spec for the query
34+
language.
35+
36+
## Local hot-swap loop
37+
38+
For interactive testing inside Flow Launcher, run from the project directory:
39+
40+
```powershell
41+
.\Flow.ConfluenceSearch\Start.ps1
42+
```
43+
44+
The script:
45+
46+
1. Stops the `Flow.Launcher` process.
47+
2. Runs `dotnet build` (Debug by default; pass `-BuildConfig Release` to
48+
switch).
49+
3. Copies `bin\Debug\net9.0-windows\*` into
50+
`%APPDATA%\FlowLauncher\Plugins\Confluence Search-1.0.1`.
51+
4. Restarts `%LOCALAPPDATA%\FlowLauncher\Flow.Launcher.exe`.
52+
53+
> The default plugin folder name embeds the version (`1.0.1`). When the
54+
> manifest version changes, either pass `-PluginFolderName "Confluence Search-x.y.z"`
55+
> or update the script default — otherwise new builds keep landing in the
56+
> old folder.
57+
58+
## Producing a release ZIP
59+
60+
```powershell
61+
.\Flow.ConfluenceSearch\Build-Plugin.ps1
62+
```
63+
64+
This builds in Release, copies the required DLLs and assets into
65+
`dist\temp\Flow.ConfluenceSearch\`, zips them as
66+
`Flow.ConfluenceSearch-v<version>.zip`, and prints the resulting path. The
67+
ZIP can be installed via Flow Launcher → Settings → Plugins → Install Plugin.
68+
69+
## CI / release
70+
71+
- `.github/workflows/build-action.yml` — builds every PR via
72+
`dotnet publish ... -r win-x64 --no-self-contained` and uploads the result
73+
as an artifact.
74+
- `.github/workflows/publish-action.yml` — on push, reads `Version` from
75+
`Flow.ConfluenceSearch/plugin.json`, publishes the build, and creates a
76+
GitHub release tagged `v<Version>` if the version differs from the
77+
latest existing release.
78+
79+
> **Heads-up:** the publish workflow's `on: push:` is currently configured
80+
> for the `master` branch, while the repo's default branch is `main`.
81+
> Pushes to `main` therefore do not trigger a release; bumping
82+
> `plugin.json` Version on `main` has no effect until the trigger is
83+
> updated to include `main`.
84+
85+
## Versioning
86+
87+
The single source of truth for the plugin version is the `Version` field in
88+
`Flow.ConfluenceSearch/plugin.json`. Bump it before merging a release-worthy
89+
change. The publish workflow tags `v<Version>` and uploads the ZIP under
90+
that name.
91+
92+
> The plugin GUID in `plugin.json` (`EE691863-…`) and the one hard-coded in
93+
> `Build-Plugin.ps1` (`BD32A62C-…`) currently differ. Flow Launcher reads
94+
> the manifest, so the discrepancy is harmless today, but the script's
95+
> value should be aligned the next time it's touched.
96+
97+
## Code conventions
98+
99+
- Primary constructors are used throughout for DI (e.g.
100+
`Searcher(IConfluenceSearchClient ..., …) : ISearcher`).
101+
- Most types are `internal` and exposed to tests via `InternalsVisibleTo`;
102+
prefer `internal` for new types unless they're genuinely part of the
103+
public surface.
104+
- DTOs use `System.Text.Json` with `[JsonPropertyName(...)]`. Don't pull in
105+
Newtonsoft.Json.

0 commit comments

Comments
 (0)