Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 70 additions & 0 deletions .agents/skills/add-event/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
name: add-event
description: Attach a new Omeka S event listener in Module.php. Invoke with a description, e.g. /add-event log media updates
---

Attach a new event listener for: $ARGUMENTS

## Steps

### 1. Register in `Module::attachListeners()`

```php
public function attachListeners(SharedEventManagerInterface $sharedEventManager)
{
// ... existing listeners ...

$sharedEventManager->attach(
'Omeka\Api\Adapter\MediaAdapter', // identifier (see table below)
'api.update.post', // event name
[$this, 'handleMyEvent']
);
}
```

### 2. Add the handler method on `Module`

```php
public function handleMyEvent(Event $event): void
{
$services = $this->getServiceLocator();
$logger = $services->get('Omeka\Logger');

/** @var \Omeka\Api\Request $request */
$request = $event->getParam('request');
/** @var \Omeka\Entity\Media $entity */
$entity = $event->getParam('entity'); // available on api.*.post events
/** @var \Omeka\Api\Response $response */
$response = $event->getParam('response'); // available on api.*.post events

// your logic here
}
```

### Common event identifiers

| Identifier | Fires for |
|---|---|
| `Omeka\Api\Adapter\MediaAdapter` | Media CRUD operations |
| `Omeka\Api\Adapter\ItemAdapter` | Item CRUD operations |
| `Omeka\Controller\Admin\Media` | Admin media pages |
| `Omeka\Controller\Admin\Item` | Admin item pages |
| `Omeka\Controller\Site\Item` | Public item pages |
| `*` | All controllers (use sparingly) |

### Common event names

| Event | When | Key params |
|---|---|---|
| `api.hydrate.post` | After entity hydration from request | `request`, `entity` |
| `api.create.post` | After entity created | `request`, `entity`, `response` |
| `api.update.post` | After entity updated | `request`, `entity`, `response` |
| `api.delete.pre` | Before entity deleted | `request`, `entity` |
| `view.show.after` | After show view rendered | view renderer as target |
| `view.layout` | Every page layout | view renderer as target |

### Notes
- `$event->getTarget()` returns the view renderer on `view.*` events and the adapter on `api.*` events.
- Get the view in `view.*` handlers: `$view = $event->getTarget();`
- Always check the entity type before acting: `if (!$this->isExeLearningFile($media)) return;`
- Services available via `$this->getServiceLocator()` inside `Module` methods.
85 changes: 85 additions & 0 deletions .agents/skills/add-route/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
name: add-route
description: Add a new route and controller action to this Omeka S module. Invoke with a short description, e.g. /add-route admin export endpoint
---

Add a new route and controller action for: $ARGUMENTS

## Steps

### 1. Add the route in `config/module.config.php`

**Public/API route** (standalone):
```php
'router' => [
'routes' => [
'exelearning-myroute' => [
'type' => \Laminas\Router\Http\Segment::class,
'options' => [
'route' => '/exelearning/my-route[/:id]',
'constraints' => ['id' => '\d+'],
'defaults' => [
'__NAMESPACE__' => 'ExeLearning\Controller',
'controller' => 'MyController',
'action' => 'myAction',
],
],
],
],
],
```

**Admin child route** (under `/admin`):
```php
'admin' => [
'child_routes' => [
'exelearning-myroute' => [
'type' => \Laminas\Router\Http\Segment::class,
'options' => [
'route' => '/exelearning/my-route[/:id]',
'constraints' => ['id' => '\d+'],
'defaults' => [
'__NAMESPACE__' => 'ExeLearning\Controller',
'controller' => 'MyController',
'action' => 'myAction',
],
],
],
],
],
```

Use `Literal` for fixed paths, `Segment` for paths with `:param`, `Regex` for paths needing slashes inside a segment (like `exelearning-content`).

### 2. Add the action to the controller

In `src/Controller/MyController.php`:
```php
public function myActionAction()
{
// Check ACL if needed:
$acl = $this->getServiceLocator()->get('Omeka\Acl');
if (!$acl->userIsAllowed('Omeka\Entity\Media', 'read')) {
return $this->redirect()->toRoute('login');
}

$id = $this->params('id');
// ...
return new \Laminas\View\Model\ViewModel(['data' => $data]);
}
```

For JSON responses: `return new \Laminas\View\Model\JsonModel(['key' => 'value']);`

### 3. Add the view template (if not JSON)

Create `view/exe-learning/my-controller/my-action.phtml`.

### 4. Generate the URL in views/JS

PHP: `$this->url('exelearning-myroute', ['id' => $id])`
JS: Use `$request->getBasePath()` prefix — the module supports playground prefix environments.

### 5. Write a unit test

Add a controller test in `test/ExeLearningTest/Controller/` following `ApiControllerTest.php` as the example.
51 changes: 51 additions & 0 deletions .agents/skills/add-service/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
name: add-service
description: Add a new Laminas service (with factory) to this Omeka S module. Invoke with the service name, e.g. /add-service MyNewService
---

Add a new service called $ARGUMENTS following the patterns in this module.

## Steps

1. **Create `src/Service/$ARGUMENTS.php`** — the service class in namespace `ExeLearning\Service`.

2. **Create `src/Service/${ARGUMENTS}Factory.php`** — implements `Laminas\ServiceManager\Factory\FactoryInterface`:
```php
namespace ExeLearning\Service;
use Interop\Container\ContainerInterface;
use Laminas\ServiceManager\Factory\FactoryInterface;

class ${ARGUMENTS}Factory implements FactoryInterface
{
public function __invoke(ContainerInterface $services, $requestedName, array $options = null)
{
// pull dependencies from $services:
// $services->get('Omeka\ApiManager')
// $services->get('Omeka\EntityManager')
// $services->get('Omeka\Logger')
// $services->get('Config')
return new $ARGUMENTS(/* dependencies */);
}
}
```

3. **Register in `config/module.config.php`** under `service_manager.factories`:
```php
'service_manager' => [
'factories' => [
Service\$ARGUMENTS::class => Service\${ARGUMENTS}Factory::class,
],
],
```

4. **Inject where needed** — in a controller factory retrieve it with:
```php
$myService = $services->get(\ExeLearning\Service\$ARGUMENTS::class);
```

5. **Write a unit test** in `test/ExeLearningTest/Service/` using PHPUnit. Add stubs to `test/Stubs/` for any new Omeka/Laminas collaborators not already stubbed.

## Notes
- Factories are excluded from coverage requirements (see phpunit.xml).
- Common Omeka services: `Omeka\ApiManager`, `Omeka\EntityManager`, `Omeka\Logger`, `Omeka\Settings`, `Omeka\Acl`.
- Use `$services->get('Config')` for module config values.
17 changes: 17 additions & 0 deletions .agents/skills/i18n/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: i18n
description: Run the full translation workflow — extract strings, update .po files, check for untranslated strings, and compile .mo files.
---

Run the full i18n pipeline in sequence. Stop and report any failure immediately.

```bash
make i18n
```

This runs: `generate-pot``update-po``check-untranslated``compile-mo`.

Report:
- Whether each step passed or failed
- Any untranslated strings found (file and string key)
- Confirmation that .mo files were compiled successfully
13 changes: 13 additions & 0 deletions .agents/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: release
description: Package the module for distribution. Invoke with a version number, e.g. /release 1.2.3
disable-model-invocation: true
---

Run the packaging command with the version provided in $ARGUMENTS:

```bash
make package VERSION=$ARGUMENTS
```

Report the output path of the generated .zip file and confirm it completed successfully.
16 changes: 16 additions & 0 deletions .agents/skills/verify/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: verify
description: Run the full CI verification pipeline locally — lint, unit tests, and 90% coverage check. Use after making changes to confirm they are ready.
---

Run the following commands in sequence. Stop and report any failure immediately.

```bash
make lint
make test-coverage
```

Report:
- Whether lint passed or failed (and which files had violations)
- Whether tests passed and the coverage percentage achieved
- Any failures with the relevant error output
3 changes: 3 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Omeka S eXeLearning Plugin — Copilot Instructions

All AI agent instructions live in [`AGENTS.md`](../AGENTS.md) at the repository root. Read that file before working on this codebase.
13 changes: 13 additions & 0 deletions .github/workflows/check-editor-releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ jobs:
git commit -m "Update editor version to ${{ steps.check.outputs.tag }}"
git push

- name: Build Playground URL for this release
if: steps.check.outputs.found == 'true'
id: playground
run: |
RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/${{ steps.version.outputs.tag }}/ExeLearning-${{ steps.version.outputs.version }}.zip"
BLUEPRINT=$(jq --arg url "$RELEASE_URL" '.modules[0].source.url = $url' blueprint.json)
ENCODED=$(echo "$BLUEPRINT" | base64 -w 0 | tr '+/' '-_' | tr -d '=')
echo "url=https://ateeducacion.github.io/omeka-s-playground/?blueprint-data=${ENCODED}" >> $GITHUB_OUTPUT

- name: Create GitHub Release
if: steps.check.outputs.found == 'true'
uses: softprops/action-gh-release@v2
Expand All @@ -90,6 +99,10 @@ jobs:
name: "${{ steps.version.outputs.tag }}"
body: |
Automated build with eXeLearning editor ${{ steps.version.outputs.tag }}.

---

<a href="${{ steps.playground.outputs.url }}"><img src="https://raw.githubusercontent.com/ateeducacion/omeka-s-playground/main/ogimage.png" alt="Try in Omeka-S Playground" width="220"></a>
files: ExeLearning-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
92 changes: 92 additions & 0 deletions .github/workflows/pr-playground-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
name: PR Playground Preview

on:
pull_request:
types: [opened, synchronize, reopened, edited]

permissions:
contents: read
pull-requests: write

jobs:
preview:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Post or update playground preview comment
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const marker = '<!-- omeka-s-playground-preview -->';
const owner = context.repo.owner;
const repo = context.repo.repo;
const headRef = context.payload.pull_request.head.ref;
const prNumber = context.payload.pull_request.number;

// Read the root blueprint and replace the module URL with the PR branch ZIP
const blueprint = JSON.parse(fs.readFileSync('blueprint.json', 'utf8'));
const prZipUrl = `https://github.com/${owner}/${repo}/archive/refs/heads/${headRef}.zip`;
if (blueprint.modules) {
for (const mod of blueprint.modules) {
if (mod.source && mod.source.url && mod.source.url.includes(`${owner}/${repo}`)) {
mod.source.url = prZipUrl;
}
}
}

// Encode as base64url
function toBase64url(str) {
return Buffer.from(str)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

const encoded = toBase64url(JSON.stringify(blueprint));
const playgroundUrl = `https://ateeducacion.github.io/omeka-s-playground/?blueprint-data=${encoded}`;
const imageUrl = 'https://raw.githubusercontent.com/ateeducacion/omeka-s-playground/refs/heads/main/ogimage.png';

const body = [
marker,
'## omeka-s Playground Preview',
'',
`<a href="${playgroundUrl}">`,
` <img src="${imageUrl}" alt="Open this PR in Omeka-S Playground" width="220">`,
'</a><br>',
`<small><a href="${playgroundUrl}">Try this PR in your browser</a></small>`,
'',
'',
'> ⚠️ The embedded eXeLearning editor is not included in this preview. You can install it from **Modules > ExeLearning > Configure** using the "Download & Install Editor" button. All other module features (ELPX upload, viewer, preview) work normally.',
].join('\n');

// Search for an existing comment with the marker
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
});

const existing = comments.find(c => c.body && c.body.includes(marker));

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
core.info(`Updated existing preview comment #${existing.id}`);
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
core.info('Created new preview comment');
}
Loading
Loading