fix: register ZiggyVue plugin for route() in Vue templates
- Add ZiggyVue plugin to app.js setup (fixes 'route is not a function' in all Vue template usages) - Add ziggy-js as production dependency (was missing) - Add CSRF meta tag to app.blade.php - Add date formatting helpers to Services/Index.vue - Name api.songs resource route to avoid Ziggy collision - Increase Playwright timeout to 90s for CI stability - Reduce sync test polling from 325 to 50 attempts
This commit is contained in:
parent
83da54215e
commit
27c6454f1b
22
.sisyphus/notepads/cts-herd-playwright/issues.md
Normal file
22
.sisyphus/notepads/cts-herd-playwright/issues.md
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
## [2026-03-01] Task 5: ZiggyVue Plugin Missing
|
||||||
|
|
||||||
|
**Severity**: HIGH — Blocks all Vue component rendering that uses `route()`
|
||||||
|
**File**: `resources/js/app.js`
|
||||||
|
**Error**: `TypeError: o.route is not a function` on every page using `route()` in templates
|
||||||
|
|
||||||
|
**Root Cause**: `@routes` blade directive provides `window.route` global, but Vue 3 `<script setup>` templates resolve against component render proxy, not `window`. The `ZiggyVue` plugin that bridges this gap is NOT registered in `app.js`.
|
||||||
|
|
||||||
|
**Impact**: Login page button, navigation links, and any template using `route()` fail to render client-side.
|
||||||
|
|
||||||
|
**Fix Required**:
|
||||||
|
```js
|
||||||
|
// In resources/js/app.js
|
||||||
|
import { ZiggyVue } from 'ziggy-js';
|
||||||
|
|
||||||
|
createApp({ render: () => h(App, props) })
|
||||||
|
.use(plugin)
|
||||||
|
.use(ZiggyVue)
|
||||||
|
.mount(el);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Workaround Applied**: Auth setup uses `page.request.post('/dev-login')` instead of clicking the UI button.
|
||||||
|
|
@ -311,3 +311,42 @@ ### VERDICT: ✅ PRODUCTION READY
|
||||||
Ready for deployment and production use.
|
Ready for deployment and production use.
|
||||||
|
|
||||||
**PROJECT COMPLETE** 🎉
|
**PROJECT COMPLETE** 🎉
|
||||||
|
|
||||||
|
## [2026-03-01] T3: UserFactory OAuth Fields
|
||||||
|
|
||||||
|
- UserFactory `definition()` must include all 4 OAuth fields from User model `$fillable` array
|
||||||
|
- `churchtools_id`: Use `fake()->unique()->numberBetween(1000, 99999)` to mimic real CTS IDs
|
||||||
|
- `avatar`: Set to `null` (realistic default from OAuth callback when no image available)
|
||||||
|
- `churchtools_groups` and `churchtools_roles`: Must be empty arrays `[]` (not strings) because User model casts them as `'array'` type
|
||||||
|
- Factory pattern: All 4 fields added to `definition()` return array alongside existing name/email/password fields
|
||||||
|
- Verification: `php artisan tinker` confirms factory creates users with all fields populated correctly
|
||||||
|
- Tests: All 174 tests pass (905 assertions) — no regressions from factory changes
|
||||||
|
|
||||||
|
## Task 1: Herd Environment Configuration (2026-03-01)
|
||||||
|
|
||||||
|
### What Was Done
|
||||||
|
- Updated `.env.example` line 5: `APP_URL=http://localhost:8000` → `APP_URL=http://cts-work.test`
|
||||||
|
- Updated `.env.example` line 77: `CHURCHTOOLS_REDIRECT_URI=http://localhost:8000/auth/churchtools/callback` → `http://cts-work.test/auth/churchtools/callback`
|
||||||
|
- Executed `php artisan config:clear` to flush cached configuration
|
||||||
|
- Executed `npm run build` to generate production assets (790 modules, 1.62s build time)
|
||||||
|
- Executed `php artisan migrate` (no migrations needed, schema already current)
|
||||||
|
- Verified login page loads at http://cts-work.test/login with HTTP 200
|
||||||
|
|
||||||
|
### Key Learnings
|
||||||
|
1. **Herd Integration**: Herd is already configured at http://cts-work.test (PHP 8.4, Herd 1.17.0)
|
||||||
|
2. **Static Build Required**: Using `npm run build` for static assets instead of Vite HMR dev server
|
||||||
|
3. **Vue/Inertia Rendering**: Login component (Auth/Login) is rendered client-side by Vue, so "Anmelden" text won't appear in raw HTML curl output
|
||||||
|
4. **Build Output**: Assets are generated in `public/build/` with manifest.json for asset versioning
|
||||||
|
5. **Database**: SQLite is default, migrations are already applied
|
||||||
|
|
||||||
|
### Verification Success Criteria Met
|
||||||
|
- ✓ Configuration files updated with Herd URLs
|
||||||
|
- ✓ All artisan commands executed successfully
|
||||||
|
- ✓ npm build completed without errors
|
||||||
|
- ✓ Login page returns HTTP 200
|
||||||
|
- ✓ Vue/Inertia app properly initialized with correct component
|
||||||
|
- ✓ Evidence saved to `.sisyphus/evidence/task-1-herd-login-page.txt`
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Task 2 will likely involve testing OAuth login flow with ChurchTools
|
||||||
|
- May need to configure CTS_API_TOKEN and CHURCHTOOLS credentials for full testing
|
||||||
|
|
|
||||||
24
package-lock.json
generated
24
package-lock.json
generated
|
|
@ -4,6 +4,9 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"ziggy-js": "^2.6.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@inertiajs/vue3": "^2.0.0",
|
"@inertiajs/vue3": "^2.0.0",
|
||||||
"@jaxtheprime/vue3-dropzone": "^1.1.0",
|
"@jaxtheprime/vue3-dropzone": "^1.1.0",
|
||||||
|
|
@ -2701,6 +2704,18 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qs-esm": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs-esm/-/qs-esm-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-8jbjCR0PPbqoQcv83C2K/zvVeytRPwPpt3WPDbq51qyLAxcWGtXVRjSe6GHtLCoVbg9+NEFkv7GyUxqjcDIJzw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
|
@ -3190,6 +3205,15 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ziggy-js": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-2.6.1.tgz",
|
||||||
|
"integrity": "sha512-cxsaEU5l7bZD+axkk6TurjDLrW1UKnxJ/BrhoGxkYe7+mFfNg2dKnmXOG2bwcXJHQ8NmGJ5dwZ99T5FOtPSsyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"qs-esm": "^7.0.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,8 @@
|
||||||
"vite": "^7.0.0",
|
"vite": "^7.0.0",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-draggable-plus": "^0.6.0"
|
"vue-draggable-plus": "^0.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ziggy-js": "^2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export default defineConfig({
|
||||||
testDir: './tests/e2e',
|
testDir: './tests/e2e',
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
timeout: 30000,
|
timeout: 90000,
|
||||||
expect: {
|
expect: {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,28 @@ const confirmWarnings = ref([])
|
||||||
const confirmServiceId = ref(null)
|
const confirmServiceId = ref(null)
|
||||||
const finalizing = ref(false)
|
const finalizing = ref(false)
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '—'
|
||||||
|
const d = new Date(dateStr)
|
||||||
|
return d.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(dateStr) {
|
||||||
|
if (!dateStr) return '—'
|
||||||
|
const d = new Date(dateStr)
|
||||||
|
return d.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
toastMessage.value = message
|
toastMessage.value = message
|
||||||
toastType.value = type
|
toastType.value = type
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
import { ZiggyVue } from 'ziggy-js';
|
||||||
import { createApp, h } from 'vue';
|
import { createApp, h } from 'vue';
|
||||||
import { createInertiaApp } from '@inertiajs/vue3';
|
import { createInertiaApp } from '@inertiajs/vue3';
|
||||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||||
|
|
@ -16,6 +17,7 @@ createInertiaApp({
|
||||||
setup({ el, App, props, plugin }) {
|
setup({ el, App, props, plugin }) {
|
||||||
return createApp({ render: () => h(App, props) })
|
return createApp({ render: () => h(App, props) })
|
||||||
.use(plugin)
|
.use(plugin)
|
||||||
|
.use(ZiggyVue)
|
||||||
.mount(el);
|
.mount(el);
|
||||||
},
|
},
|
||||||
progress: {
|
progress: {
|
||||||
|
|
|
||||||
1
resources/js/bootstrap.js
vendored
1
resources/js/bootstrap.js
vendored
|
|
@ -2,3 +2,4 @@ import axios from 'axios';
|
||||||
window.axios = axios;
|
window.axios = axios;
|
||||||
|
|
||||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
Route::middleware('auth:sanctum')->group(function () {
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
Route::apiResource('songs', SongController::class);
|
Route::apiResource('songs', SongController::class)->names('api.songs');
|
||||||
|
|
||||||
|
|
||||||
Route::post('/service-songs/{serviceSongId}/assign', [ServiceSongController::class, 'assignSong'])
|
Route::post('/service-songs/{serviceSongId}/assign', [ServiceSongController::class, 'assignSong'])
|
||||||
->name('api.service-songs.assign');
|
->name('api.service-songs.assign');
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ test('sync button triggers sync with loading indicator and timestamp update', as
|
||||||
// multiple times, then reload if needed.
|
// multiple times, then reload if needed.
|
||||||
let updatedTimestamp = await page.getByTestId('auth-layout-sync-timestamp').textContent();
|
let updatedTimestamp = await page.getByTestId('auth-layout-sync-timestamp').textContent();
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
const maxAttempts = 325; // Wait up to 65 seconds (325 * 200ms)
|
const maxAttempts = 50; // Wait up to 10 seconds (50 * 200ms)
|
||||||
|
|
||||||
while (updatedTimestamp === initialTimestamp && attempts < maxAttempts) {
|
while (updatedTimestamp === initialTimestamp && attempts < maxAttempts) {
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue