Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7e633ca
Merge branch 'main' into develop
jeppekroghitk Jun 30, 2026
69c991e
Rename from itk-project-database to itk-projects
jeppekroghitk Jul 3, 2026
88c5cb1
Updated composer.lock
jeppekroghitk Jul 3, 2026
2426adc
Updated README
jeppekroghitk Jul 3, 2026
0b40992
Coding standards
jeppekroghitk Jul 3, 2026
f6333a4
Add Partner entity
jeppekroghitk Aug 13, 2026
ea61954
Add partner admin section
jeppekroghitk Aug 13, 2026
a04162b
Require one or more partners on initiatives
jeppekroghitk Aug 13, 2026
85c815d
Add partners to CSV export
jeppekroghitk Aug 13, 2026
b4e1481
Update changelog
jeppekroghitk Aug 13, 2026
3547160
Make partners optional on initiatives
jeppekroghitk Aug 13, 2026
3a46533
Validate partner name and website
jeppekroghitk Aug 13, 2026
5a73b23
Preload partners in CSV export
jeppekroghitk Aug 13, 2026
2deec1c
Assert partner ordering in repository test
jeppekroghitk Aug 13, 2026
5f9c40a
Fix partner fixture websites
jeppekroghitk Aug 13, 2026
3920d46
Update changelog
jeppekroghitk Aug 13, 2026
58baf41
Add partner usage queries
jeppekroghitk Aug 13, 2026
8462e71
Add confirm dialog controller
jeppekroghitk Aug 13, 2026
3e97874
Confirm partner deletion with affected initiatives
jeppekroghitk Aug 13, 2026
79012b2
Update changelog
jeppekroghitk Aug 13, 2026
744a6c6
Add query string to url
jeppekroghitk Aug 14, 2026
c6d1567
Add tests
jeppekroghitk Aug 14, 2026
292df59
Updated changelog
jeppekroghitk Aug 14, 2026
9b2833f
Add a topic field to initiatives
jeppekroghitk Aug 14, 2026
cac9f21
Drop the example from the topic help text
jeppekroghitk Aug 17, 2026
46b2028
Address the review on partners, and confirm deletes one way
jeppekroghitk Aug 18, 2026
b4ed187
Clamp over-long partner names instead of rejecting them
jeppekroghitk Aug 20, 2026
6978660
Close open dialogs before Turbo caches the page
jeppekroghitk Aug 20, 2026
557d612
Name the confirm dialog for screen readers
jeppekroghitk Aug 20, 2026
209d4e8
Trim partner comments
jeppekroghitk Aug 20, 2026
bf21165
Describe the two migrations that shipped without a description
jeppekroghitk Aug 21, 2026
5d047d5
Merge pull request #26 from itk-dev/feature/8087-add-deeplinking
jeppekroghitk Aug 24, 2026
b3ccb35
Merge branch 'develop' into feature/8121-initiative-topic
jeppekroghitk Aug 24, 2026
ca9c699
Merge branch 'develop' into feature/8085-add-partners-entity
jeppekroghitk Aug 24, 2026
1fc30c7
Merge pull request #25 from itk-dev/feature/8085-add-partners-entity
jeppekroghitk Aug 24, 2026
04a8431
Fixed changelog
jeppekroghitk Aug 24, 2026
e560de9
Merge branch 'develop' into feature/8121-initiative-topic
jeppekroghitk Aug 24, 2026
75ff932
Merge pull request #27 from itk-dev/feature/8121-initiative-topic
jeppekroghitk Aug 24, 2026
15872cd
Updated changelog
jeppekroghitk Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
COMPOSE_PROJECT_NAME=itk-project-database
COMPOSE_DOMAIN=itk-project-database.local.itkdev.dk
COMPOSE_PROJECT_NAME=itk-projects
COMPOSE_DOMAIN=itk-projects.local.itkdev.dk
ITKDEV_TEMPLATE=symfony-8

# In all environments, the following files are loaded if they exist,
Expand Down
59 changes: 37 additions & 22 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-08-24

* [PR-27](https://github.com/itk-dev/itk-projects/pull/27)
Add a Topic field to initiatives, naming the wider programme an initiative is
part of. Shown under the title on the form and on the initiative page, covered
by the free-text filter, and included in the CSV export.
* [PR-26](https://github.com/itk-dev/itk-projects/pull/26)
Reflect the initiative list's filters in the address bar so they can be
deeplinked.
* [PR-25](https://github.com/itk-dev/itk-projects/pull/25)
Add a Partner entity (name, description, website) with an admin CRUD, and
require every initiative to have at least one partner — attached through a
searchable multiselect that can create new partners on the fly.

## [0.2.0] - 2026-06-30

* [PR-23](https://github.com/itk-dev/itk-project-database/pull/23)
* [PR-23](https://github.com/itk-dev/itk-projects/pull/23)
Introduce reusable Twig components (page header, card header, empty state, KPI)
to replace repeated markup.
* [PR-22](https://github.com/itk-dev/itk-project-database/pull/22)
* [PR-22](https://github.com/itk-dev/itk-projects/pull/22)
Use the ITK logo in the nav and login, tidy the dashboard header, and refresh
the login screen.
* [PR-21](https://github.com/itk-dev/itk-project-database/pull/21)
* [PR-21](https://github.com/itk-dev/itk-projects/pull/21)
Add a first-login guided tour where Glimt walks new users through the platform,
dashboard, creating initiatives and the admin panel.
* [PR-20](https://github.com/itk-dev/itk-project-database/pull/20)
* [PR-20](https://github.com/itk-dev/itk-projects/pull/20)
Minor improvements to the user menu styling.
* [PR-19](https://github.com/itk-dev/itk-project-database/pull/19)
* [PR-19](https://github.com/itk-dev/itk-projects/pull/19)
Auto-upload files with a progress bar and image preview, view images in an
in-page lightbox, and refresh the media field styling.
* [PR-18](https://github.com/itk-dev/itk-project-database/pull/18)
* [PR-18](https://github.com/itk-dev/itk-projects/pull/18)
Rework the dashboard with an outstanding-work panel and a redesigned activity
feed, add help text to every graph, and fix the mascot's finish nudges.
* [PR-17](https://github.com/itk-dev/itk-project-database/pull/17)
* [PR-17](https://github.com/itk-dev/itk-projects/pull/17)
Fix the mascot nudges that never appeared, nudge users to finish incomplete
contacts with a link to their edit page.
* [PR-16](https://github.com/itk-dev/itk-project-database/pull/16)
* [PR-16](https://github.com/itk-dev/itk-projects/pull/16)
Turn the strategies and tags fields into a searchable,
shared tag pool where new entries are capitalised and reused as suggestions.
* [PR-15](https://github.com/itk-dev/itk-project-database/pull/15)
* [PR-15](https://github.com/itk-dev/itk-projects/pull/15)
Make Kategori a user-defined Area entity with an admin CRUD, replacing the
fixed Category enum.

## [0.1.0] - 2026-06-26

* [PR-9](https://github.com/itk-dev/itk-project-database/pull/9)
* [PR-9](https://github.com/itk-dev/itk-projects/pull/9)
Make department a managed entity and move it and contacts to the admin section
* [PR-7](https://github.com/itk-dev/itk-project-database/pull/7)
* [PR-7](https://github.com/itk-dev/itk-projects/pull/7)
Add a mascot motivating users to create and complete initiatives
* [PR-6](https://github.com/itk-dev/itk-project-database/pull/6)
* [PR-6](https://github.com/itk-dev/itk-projects/pull/6)
Mark udfyldningsgrad fields with a star that flies into a progress trophy
* [PR-5](https://github.com/itk-dev/itk-project-database/pull/5)
* [PR-5](https://github.com/itk-dev/itk-projects/pull/5)
Improve initiative list, search and filters using partial page rendering
* [PR-8](https://github.com/itk-dev/itk-project-database/pull/8)
* [PR-8](https://github.com/itk-dev/itk-projects/pull/8)
Adopt itk-dev/entity-bundle: ULID identifiers + shared blamable/timestampable
* [PR-10](https://github.com/itk-dev/itk-project-database/pull/10)
* [PR-10](https://github.com/itk-dev/itk-projects/pull/10)
Correct docker compose setup.
* [PR-4](https://github.com/itk-dev/itk-project-database/pull/4)
* [PR-4](https://github.com/itk-dev/itk-projects/pull/4)
Add Chart.js graphs to the dashboard
* [PR-3](https://github.com/itk-dev/itk-project-database/pull/3)
* [PR-3](https://github.com/itk-dev/itk-projects/pull/3)
Add real-time activity feed with autosave
* [PR-2](https://github.com/itk-dev/itk-project-database/pull/2)
* [PR-2](https://github.com/itk-dev/itk-projects/pull/2)
Add Symfony UX Turbo and stimulus.
* [PR-1](https://github.com/itk-dev/itk-project-database/pull/1)
* [PR-1](https://github.com/itk-dev/itk-projects/pull/1)
Initial Symfony 8 rebuild of the project database.

[Unreleased]: https://github.com/itk-dev/itk-project-database/compare/0.2.0...HEAD
[0.2.0]: https://github.com/itk-dev/itk-project-database/compare/0.1.09i876rbhn%3E%20gvfcdevgbhjgnybtfr%20v8decsxz...0.2.0
[0.1.0]: <https://github.com/itk-dev/itk-project-database/releases/tag/0.1.09i876rbhn> gvfcdevgbhjgnybtfr v8decsxz
[Unreleased]: https://github.com/itk-dev/itk-projects/compare/0.3.0...HEAD
[0.3.0]: https://github.com/itk-dev/itk-projects/compare/0.2.0...0.3.0
[0.2.0]: https://github.com/itk-dev/itk-projects/compare/0.1.09i876rbhn%3E%20gvfcdevgbhjgnybtfr%20v8decsxz...0.2.0
[0.1.0]: <https://github.com/itk-dev/itk-projects/releases/tag/0.1.09i876rbhn> gvfcdevgbhjgnybtfr v8decsxz
73 changes: 13 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
# Project database
# ITK Projects

A Symfony application for registering and browsing municipal **initiatives** and
their **contacts**. It is a rebuild of the previous Drupal-based project
database, focused on a friendlier interface for creating and getting an overview
A Symfony application for registering and browsing municipal **initiatives**.
It is a rebuild of the previous Drupal-based [project-database](https://github.com/itk-dev/project-database)
with an accompanied react application for graph visualizations [project-database-app](https://github.com/itk-dev/project-database-app),
focused on a friendlier interface for creating and getting an overview
of initiatives.

The project follows the itk-dev
[`symfony` Docker template](https://github.com/itk-dev/devops_itkdev-docker) and
runs on PHP 8.4 / Symfony 8.

## Features

- Dashboard with key figures and a status overview of all initiatives.
- List of initiatives with full-text search, faceted filters, column sorting,
pagination and CSV export.
- Create and edit initiatives, including inline creation of contacts,
free-tagging of tags, stakeholders and strategies, and image/file uploads.
- Contact management.
- Private file/image uploads, served only to signed-in users.
- Local username/password login with user administration for administrators.
- Bilingual interface (Danish and English).

## Requirements

- [Docker](https://www.docker.com/) and the itk-dev
Expand All @@ -36,17 +25,8 @@ development fixtures):
task install
```

Without Task:

```sh
docker compose up --detach
docker compose exec phpfpm composer install
docker compose exec phpfpm bin/console doctrine:migrations:migrate --no-interaction
docker compose exec phpfpm bin/console doctrine:fixtures:load --no-interaction
```

The site is served on the domain configured in `.env`
(`COMPOSE_DOMAIN`, e.g. `https://itk-project-database.local.itkdev.dk`).
(`COMPOSE_DOMAIN`, e.g. `https://itk-projects.local.itkdev.dk`).

### Signing in

Expand All @@ -61,41 +41,14 @@ Create an administrator manually with:
task create-admin -- you@example.com "Your Name"
```

## Access control

The application uses a single, flat trust model: authentication is required for
everything (the firewall protects `^/`), and **every authenticated user is fully
trusted**. Any signed-in user (`ROLE_USER`) can create, view, edit and delete any
initiative or contact, and can download any uploaded image or attachment by id.

The only elevated capability is **user administration** (`/admin/**`), which
requires `ROLE_ADMIN`.

This is intentional: the project database is an internal tool for a small,
trusted group of municipal editors, so per-record ownership or per-action
authorization would add complexity without a real security benefit. Uploaded
files are stored outside the web root and served only through the authenticated
`MediaController`, so they are never anonymously reachable — but they are not
restricted between authenticated users.

If a future requirement calls for restricting who may edit/delete a given record
(or read a given file), introduce a Symfony [Voter](https://symfony.com/doc/current/security/voters.html)
rather than loosening or working around the flat model.

## Development

```sh
task # list all tasks
task console -- <command> # run a Symfony console command
task coding-standards:fix # apply coding standards
task static-analysis # run PHPStan
task test # run the test suite
task ci # run everything CI runs
task # list all tasks
task console -- <command> # run a Symfony console command
task compose -- <command> # run a composer command
task coding-standards:check # Check coding standards
task coding-standards:apply # Apply coding standards
task static-analysis # run PHPStan
task test # run the test suite
```

### Note on controlled vocabularies

The controlled vocabularies (status, category, type, organisational anchoring,
endorsement author and funding) are modelled as PHP enums in `src/Enum/` with
placeholder values. Adjust the enum cases and their translations
(`translations/messages.*.yaml`) to match the real domain values.
17 changes: 17 additions & 0 deletions assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ function initContactSelect() {
);
}

function initPartnerSelect() {
initCreatableSelect(
"[data-partner-select]",
"partnerPool",
(value) => value,
);
}

// One delegated handler on the document (which survives Turbo navigations and
// cache restores) both opens the menu — when the click lands on the toggle —
// and closes it on any outside click. Delegation avoids per-page binding, which
Expand All @@ -142,6 +150,7 @@ document.addEventListener("click", (event) => {
document.addEventListener("turbo:load", () => {
initCollections();
initContactSelect();
initPartnerSelect();
initTermSelect();
});

Expand All @@ -150,3 +159,11 @@ document.addEventListener("turbo:load", () => {
document.addEventListener("turbo:frame-load", () => {
initCollections();
});

// Submitting from inside a confirm dialog caches the page with the dialog still
// open; restoring that snapshot would render it inline, out of the top layer.
document.addEventListener("turbo:before-cache", () => {
document.querySelectorAll("dialog[open]").forEach((dialog) => {
dialog.close();
});
});
23 changes: 23 additions & 0 deletions assets/controllers/confirm_delete_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Controller } from "@hotwired/stimulus";

// The trigger is a plain button, so a click landing before this controller has
// hydrated does nothing; only the button inside the dialog submits.
export default class extends Controller {
static targets = ["dialog"];

open() {
this.dialogTarget.showModal();
}

cancel() {
this.dialogTarget.close();
}

// A modal dialog fills the top layer, so a click on the backdrop reports the
// dialog itself as the target; anything inside reports a descendant.
backdrop(event) {
if (event.target === this.dialogTarget) {
this.dialogTarget.close();
}
}
}
61 changes: 56 additions & 5 deletions assets/controllers/live_search_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import { Controller } from "@hotwired/stimulus";
* (data-turbo-frame), so submitting it swaps only the results — no full page
* load. Typing is debounced; selects submit on change. The submit button is
* gone: this controller drives the submit, and clear() resets the fields.
*
* The frame carries data-turbo-action="advance", so the query it was fetched
* with becomes the address bar URL — the deeplink trimQuery() keeps clean.
*/
export default class extends Controller {
static values = { debounce: { type: Number, default: 300 } };
static targets = ["form"];

static values = {
debounce: { type: Number, default: 300 },
exportUrl: String,
};

connect() {
this.timer = null;
Expand All @@ -20,21 +28,64 @@ export default class extends Controller {
submit() {
window.clearTimeout(this.timer);
this.timer = window.setTimeout(
() => this.element.requestSubmit(),
() => this.formTarget.requestSubmit(),
this.debounceValue,
);
}

// Filtering is live, so Enter has nothing left to run — and without a
// submit button the browser would submit the form itself on every press.
ignoreEnter(event) {
event.preventDefault();
}

clear() {
for (const input of this.element.querySelectorAll("input")) {
for (const input of this.formTarget.querySelectorAll("input")) {
if (!["submit", "button", "reset"].includes(input.type)) {
input.value = "";
}
}
for (const select of this.element.querySelectorAll("select")) {
for (const select of this.formTarget.querySelectorAll("select")) {
select.selectedIndex = 0;
}
window.clearTimeout(this.timer);
this.element.requestSubmit();
this.formTarget.requestSubmit();
}

// Turbo re-reads detail.url after this event, and the frame adopts the
// response URL — so dropping empty filters here is what shortens the link.
trimQuery(event) {
const url = new URL(event.detail.url);
url.search = this.query();
event.detail.url = url;
}

// Downloads a file, so it leaves Turbo behind.
exportCsv() {
const query = this.query();
window.location.assign(
query ? `${this.exportUrlValue}?${query}` : this.exportUrlValue,
);
}

// Sorting is driven by links inside the frame rather than by a form field,
// so it has to come off the URL or a filter change would reset it.
query() {
const params = new URLSearchParams();
for (const [name, value] of new FormData(this.formTarget)) {
if ("" !== value) {
params.append(name, value);
}
}

const current = new URLSearchParams(window.location.search);
for (const name of ["sort", "direction"]) {
const value = current.get(name);
if (value) {
params.set(name, value);
}
}

return params.toString();
}
}
Loading
Loading