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
113 changes: 113 additions & 0 deletions .github/scripts/process_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Processes a GitHub issue and updates reproinventory_data.yaml and
frontend/public/data/reproinventory_data.json accordingly.

Reads:
/tmp/issue_body.txt - full issue body
/tmp/issue_title.txt - issue title

Environment variables:
ISSUE_LABEL - one of: new-material, edit-material, delete-material
ISSUE_NUMBER - issue number (for logging)
"""

import os
import re
import json
import yaml

YAML_PATH = "model/reproinventory_data.yaml"
JSON_PATH = "frontend/public/data/reproinventory_data.json"

ARRAY_FIELDS = [
"tag_team", "level", "platform", "keywords", "instruction_medium",
"delivery", "language", "programming_language", "neuroimaging_software",
"imaging_modality", "quadrants", "source", "prerequisite",
]

def normalize_entry(entry):
"""Ensure all array fields are lists, not scalars."""
for field in ARRAY_FIELDS:
val = entry.get(field)
if val is not None and not isinstance(val, list):
entry[field] = [val]
return entry

label = os.environ["ISSUE_LABEL"]
issue_number = os.environ["ISSUE_NUMBER"]

with open("/tmp/issue_body.txt", "r", encoding="utf-8") as f:
issue_body = f.read()

with open("/tmp/issue_title.txt", "r", encoding="utf-8") as f:
issue_title = f.read().strip()

# Load current data
with open(YAML_PATH, "r", encoding="utf-8") as f:
data = [normalize_entry(e) for e in (yaml.safe_load(f) or [])]


def extract_yaml_block(body):
"""Extract the first ```yaml ... ``` block from the issue body."""
match = re.search(r"```yaml\s*\n(.*?)\n```", body, re.DOTALL)
if not match:
raise ValueError("No YAML block found in issue body.")
return yaml.safe_load(match.group(1))


if label == "new-material":
entry = normalize_entry(extract_yaml_block(issue_body))

# Assign a new numeric ID
numeric_ids = [e["id"] for e in data if isinstance(e.get("id"), int)]
entry["id"] = max(numeric_ids, default=0) + 1

data.append(entry)
print(f"Added new entry with ID {entry['id']}: {entry.get('course_name')}")

elif label == "edit-material":
entry = normalize_entry(extract_yaml_block(issue_body))
entry_id = entry.get("id")

replaced = False
for i, e in enumerate(data):
if str(e.get("id")) == str(entry_id):
data[i] = entry
replaced = True
break

if not replaced:
raise ValueError(f"Entry with ID '{entry_id}' not found in data.")
print(f"Updated entry ID {entry_id}: {entry.get('course_name')}")

elif label == "delete-material":
# Extract ID from issue title: "Delete material: Name (ID: 123)"
match = re.search(r"ID:\s*(\S+?)\)", issue_title)
if not match:
raise ValueError(f"Could not extract ID from issue title: {issue_title!r}")

raw_id = match.group(1)
try:
entry_id = int(raw_id)
except ValueError:
entry_id = raw_id

original_len = len(data)
data = [e for e in data if str(e.get("id")) != str(entry_id)]

if len(data) == original_len:
raise ValueError(f"Entry with ID '{entry_id}' not found in data.")
print(f"Deleted entry with ID {entry_id}")

else:
raise ValueError(f"Unknown label: {label!r}")

# Write updated YAML
with open(YAML_PATH, "w", encoding="utf-8") as f:
yaml.dump(data, f, sort_keys=False, default_flow_style=False, allow_unicode=True)

# Write updated JSON
with open(JSON_PATH, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)

print(f"Successfully processed '{label}' for issue #{issue_number}.")
65 changes: 65 additions & 0 deletions .github/workflows/create-pr-from-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Create PR from Issue

on:
issues:
types: [labeled]

jobs:
create-pr:
runs-on: ubuntu-latest
if: contains(fromJSON('["new-material", "edit-material", "delete-material"]'), github.event.label.name)

permissions:
contents: write
pull-requests: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install dependencies
run: pip install pyyaml

- name: Write issue data to files
env:
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
printf '%s' "$ISSUE_BODY" > /tmp/issue_body.txt
printf '%s' "$ISSUE_TITLE" > /tmp/issue_title.txt

- name: Process issue and update files
env:
ISSUE_LABEL: ${{ github.event.label.name }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: python .github/scripts/process_issue.py

- name: Create branch and open PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_LABEL: ${{ github.event.label.name }}
ISSUE_TITLE: ${{ github.event.issue.title }}
run: |
BRANCH="auto/${ISSUE_LABEL}-issue-${ISSUE_NUMBER}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add model/reproinventory_data.yaml frontend/public/data/reproinventory_data.json
if git diff --cached --quiet; then
echo "No changes detected — nothing to commit."
exit 1
fi
git commit -m "${ISSUE_TITLE}"
git push origin "$BRANCH"
PR_BODY=$(printf "Closes #%s\n\nAutomatically generated from issue #%s." "${ISSUE_NUMBER}" "${ISSUE_NUMBER}")
gh pr create \
--title "${ISSUE_TITLE}" \
--body "${PR_BODY}" \
--base main \
--head "${BRANCH}"
82 changes: 82 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# ReproInventory - Claude Instructions

## Project Overview

ReproInventory is a web application for browsing neuroimaging and reproducibility training materials. It is part of the ReproNim project. Users can search and filter a catalog of training resources by level, platform, format, programming language, neuroimaging software, and more.

## Tech Stack

- **Frontend:** React 19 + TypeScript + Vite
- **Styling:** Tailwind CSS v4
- **UI Components:** shadcn/ui (built on Radix UI primitives)
- **Icons:** lucide-react
- **Data:** Static JSON file served from `frontend/public/data/reproinventory_data.json`
- **Model/Schema:** YAML-based schema in `model/`, with Python scripts to generate JSON
- **Deploy:** GitHub Pages via GitHub Actions (pushes to `main` auto-deploy)

## Directory Structure

```
frontend/ # Main React app
src/
components/ # Custom components (e.g. Footer, EditMaterialDialog, AddMaterialDialog)
components/ui/ # shadcn/ui primitives (accordion, badge, button, card, etc.)
types/ # TypeScript types generated from the YAML schema
training-materials-browser.tsx # Main browser/filter UI
App.tsx # Root component
public/
data/
reproinventory_data.json # The training materials dataset

model/ # Schema and data source of truth
model.yaml # LinkML schema definition
reproinventory_data.yaml # Raw training data
reproinventory_schema.yaml # Schema
generate_reproinventory_data.py # Generates JSON from YAML
convert_yaml_to_json.py

SimpleViewer/ # Legacy Python/Flask viewer (archived, do not modify)
```

## Development Commands

All commands run from the `frontend/` directory:

```bash
npm run dev # Start local dev server
npm run build # TypeScript check + Vite build
npm run lint # ESLint
npm run preview # Preview production build locally
```

## Data Model

The data schema is defined in `model/model.yaml` (LinkML). TypeScript types in `frontend/src/types/reproinventory.ts` are generated from this schema. Key fields on `ReproInventoryEntry`:

- `id`, `course_name`, `url`, `review`, `notes`, `keywords`
- `level`, `platform`, `course_length`, `instruction_medium`, `delivery`
- `language`, `programming_language`, `neuroimaging_software`, `imaging_modality`
- `open_dataset`, `assessment`, `quadrants`, `tag_team`

Enum values are strict — always use values that match the schema.

## Code Conventions

- Use the types from `frontend/src/types/reproinventory.ts` for all data model types; do not redefine them locally (note: `AddMaterialDialog.tsx` currently duplicates types — prefer importing from the shared types file in new code).
- Use `@/` path alias for imports (e.g. `@/components/ui/button`).
- UI primitives live in `frontend/src/components/ui/` — use these rather than raw HTML elements.
- Custom components go in `frontend/src/components/`.
- The dataset is fetched at runtime from `/ReproInventory/data/reproinventory_data.json` (the GitHub Pages base path).

## Deployment

- Pushing to `main` triggers GitHub Actions which builds the frontend and deploys to GitHub Pages.
- The Vite base path is set for GitHub Pages — keep this in mind when referencing public assets.
- Do not push broken builds to `main`.

## What to Avoid

- Do not modify files in `SimpleViewer/` — it is a legacy viewer and not actively used.
- Do not change enum values without also updating `model/model.yaml` and regenerating types.
- Do not hardcode data that belongs in `reproinventory_data.json` or the YAML source.
- Do not add dependencies without good reason — the stack is intentionally minimal.
Loading
Loading