Merge branch 'main' into invites-2

This commit is contained in:
Anton Tranelis 2025-10-14 15:57:00 +02:00 committed by GitHub
commit 1ffe86848a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
164 changed files with 30093 additions and 37667 deletions

View File

@ -29,13 +29,18 @@ jobs:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version-file: '.tool-versions'
- name: Build Docker Production
run: |
mkdir -p ./data/uploads
sudo chmod 777 -R ./data
docker compose -f docker-compose.yml up -d
sleep 5
cd backend && ./seed.sh
cd backend && ./push.sh && ./seed.sh
working-directory: ${{env.WORKING_DIRECTORY}}
#build-development:
@ -51,4 +56,4 @@ jobs:
#
# - name: Build Docker Development
# run: docker compose build
# working-directory: ${{env.WORKING_DIRECTORY}}
# working-directory: ${{env.WORKING_DIRECTORY}}

211
.github/workflows/test.e2e.yml vendored Normal file
View File

@ -0,0 +1,211 @@
name: test:e2e
on: push
jobs:
cypress-e2e-tests:
name: Run E2E Tests
runs-on: ubuntu-latest
outputs:
tests-failed: ${{ steps.cypress-tests.outcome == 'failure' || steps.report-results.outputs.test_failed == 'true' }}
tests-outcome: ${{ steps.cypress-tests.outcome }}
test_failed: ${{ steps.report-results.outputs.test_failed }}
steps:
- name: Checkout code
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
- name: Set up Node.js
uses: actions/setup-node@89d709d423dc495668cd762a18dd4a070611be3f # v5.0.0
with:
node-version-file: ./.tool-versions
cache: 'npm'
cache-dependency-path: |
app/package-lock.json
lib/package-lock.json
cypress/package-lock.json
- name: Build Library
run: |
npm ci
npm run build
working-directory: ./lib
- name: Build Frontend
run: |
cp .env.dist .env
sed -i '/VITE_DIRECTUS_ADMIN_ROLE=/c\VITE_DIRECTUS_ADMIN_ROLE=8141dee8-8e10-48d0-baf1-680aea271298' .env
npm ci
npm run build
working-directory: ./app
- name: Clean Database State
run: |
# Remove any existing database data to ensure fresh state
sudo rm -rf ./data/database
mkdir -p ./data/uploads
sudo chmod 777 -R ./data
- name: Build and start all Containers
run: docker compose up -d
- name: Wait for Directus to be Ready
run: |
echo "Waiting for Directus API to be ready..."
timeout 120 bash -c 'until curl -f http://localhost:8055/server/health; do echo "Waiting for Directus..."; sleep 5; done'
echo "Directus is ready!"
- name: Seed Backend
run: |
mkdir -p ./data/uploads
sudo chmod 777 -R ./data
cd backend && ./push.sh && ./seed.sh
working-directory: ./
- name: Wait for Application to be Ready
run: |
echo "Waiting for application to be ready..."
timeout 300 bash -c 'until curl -f http://localhost:8080/login; do sleep 5; done'
echo "Application is ready!"
- name: Health Check
run: |
echo "Frontend health check:"
curl -f http://localhost:8080/login || exit 1
echo "Backend health check:"
curl -f http://localhost:8055/server/health || exit 1
- name: Install Cypress Dependencies
run: npm ci
working-directory: ./cypress
- name: Setup Display Environment for Parallel Tests
run: |
echo "Setting up display environment for parallel Cypress execution..."
# Kill any existing Xvfb processes to ensure clean state
sudo pkill Xvfb || true
# Remove any existing lock files
sudo rm -f /tmp/.X*-lock || true
# Ensure xvfb is available
which xvfb-run || (sudo apt-get update && sudo apt-get install -y xvfb)
echo "Display environment setup complete"
- name: Run E2E Tests
id: cypress-tests
run: |
# Override the npm script to use xvfb-run with display isolation
SPEC_COUNT=$(find e2e -name "*.cy.ts" | wc -l)
echo "Running $SPEC_COUNT test chunks in parallel with display isolation"
# Array to store background process PIDs
declare -a pids=()
# Launch parallel processes with isolated displays
for i in $(seq 0 $((SPEC_COUNT-1))); do
echo "Starting Cypress chunk $((i + 1))/$SPEC_COUNT on display :$((100 + i))"
(
SPLIT="$SPEC_COUNT" SPLIT_INDEX="$i" SPLIT_SUMMARY=false \
xvfb-run --server-num="$((100 + i))" \
--server-args="-screen 0 1280x720x24 -ac +extension GLX +render -noreset" \
npx cypress run --e2e --browser chromium
) &
pids+=($!)
done
# Wait for all background processes and collect exit codes
exit_code=0
for pid in "${pids[@]}"; do
if ! wait "$pid"; then
echo "Process $pid failed"
exit_code=1
fi
done
echo "All parallel test processes completed"
# Exit with failure if any test failed
if [ $exit_code -ne 0 ]; then
echo "❌ Some tests failed"
exit 1
else
echo "✅ All tests passed"
fi
working-directory: ./cypress
env:
# Disable individual cypress-split summaries to avoid conflicts
SPLIT_SUMMARY: false
- name: Upload test artifacts on failure
if: failure()
uses: actions/upload-artifact@2848b2cda0e5190984587ec6bb1f36730ca78d50 # v4.6.2
with:
name: cypress-test-results-${{ github.run_id }}
path: |
cypress/results/
cypress/screenshots/
retention-days: 7
if-no-files-found: warn
process-test-reports:
name: Process Test Reports
runs-on: ubuntu-latest
needs: cypress-e2e-tests
if: failure() && needs.cypress-e2e-tests.result == 'failure'
steps:
- name: Checkout code
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
- name: Setup Node.js
uses: actions/setup-node@89d709d423dc495668cd762a18dd4a070611be3f # v5.0.0
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: cypress/package-lock.json
- name: Install dependencies
run: npm ci
working-directory: ./cypress
- name: Download test artifacts
uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c # v5.0.0
with:
name: cypress-test-results-${{ github.run_id }}
path: ./cypress
- name: Merge JSON reports into one consolidated report
run: ./scripts/merge-reports.sh
working-directory: ./cypress
- name: Generate HTML report with screenshots
run: ./scripts/generate-html-report.sh
working-directory: ./cypress
- name: Create simple index page
run: ./scripts/create-index-page.sh
working-directory: ./cypress
env:
GITHUB_RUN_ID: ${{ github.run_id }}
GITHUB_SHA: ${{ github.sha }}
- name: Upload consolidated test report
uses: actions/upload-artifact@2848b2cda0e5190984587ec6bb1f36730ca78d50 # v4.6.2
with:
name: e2e-test-report-${{ github.run_id }}
path: cypress/results/html/
retention-days: 14
if-no-files-found: warn
- name: Upload raw test data (for debugging)
if: failure()
uses: actions/upload-artifact@2848b2cda0e5190984587ec6bb1f36730ca78d50 # v4.6.2
with:
name: e2e-raw-data-${{ github.run_id }}
path: |
cypress/results/
cypress/screenshots/
cypress/videos/
retention-days: 7
if-no-files-found: warn

34
.github/workflows/test.lint.cypress.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: test:lint:cypress
on: push
jobs:
files-changed:
name: Detect File Changes - lint - cypress
runs-on: ubuntu-latest
outputs:
cypress: ${{ steps.filter.outputs.cypress }}
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
cypress:
- '.github/workflows/test.lint.cypress.yml'
- '.github/workflows/test.e2e.yml'
- 'cypress/**/*'
lint:
if: needs.files-changed.outputs.cypress == 'true'
name: Lint - cypress
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
- uses: actions/setup-node@89d709d423dc495668cd762a18dd4a070611be3f # v5.0.0
with:
node-version-file: '.tool-versions'
- name: Lint
run: npm install && npm run lint
working-directory: cypress/

5
.gitignore vendored
View File

@ -1,2 +1,7 @@
.claude/
data/
node_modules/
cypress/node_modules/
cypress/results/
cypress/runner-results/
cypress/screenshots/

View File

@ -1 +1 @@
nodejs 20.12.1
nodejs 22.20.0

View File

@ -37,11 +37,6 @@ npm run test:unit:dev # Run Vitest in watch mode
npm run docs:generate # Generate TypeDoc documentation
```
### Root Level
```bash
./scripts/check-lint.sh # Run linting on both app and lib (used by PR hooks)
```
### Backend (Directus)
```bash
cd app
@ -94,9 +89,9 @@ npx directus-sync push --directus-url http://localhost:8055 --directus-email adm
### Testing Strategy
- **Unit Tests**: Vitest for lib components with coverage reporting
- **Component Tests**: Cypress for React component integration
- **Linting**: ESLint with TypeScript rules for code quality
- **Type Checking**: TypeScript strict mode across all packages
- **End-to-End Tests**: Cypress for testing the app's UI and user flows
### Import Conventions

17
app/package-lock.json generated
View File

@ -50,6 +50,9 @@
"typescript": "^5.0.2",
"vite": "^6.2.0",
"vite-plugin-pwa": "^0.21.1"
},
"engines": {
"node": ">=22.20.0"
}
},
"node_modules/@ampproject/remapping": {
@ -4517,13 +4520,13 @@
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@ -11545,9 +11548,9 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View File

@ -3,6 +3,9 @@
"private": true,
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=22.20.0"
},
"scripts": {
"dev": "vite --host",
"build": "tsc && vite build",

View File

@ -118,7 +118,7 @@ function App() {
setError(
typeof error === 'string'
? error
: error?.errors?.[0]?.message ||
: (error?.errors?.length > 0 ? error.errors[0]?.message : null) ||
error?.message ||
'Failed to connect to the server. Please check your connection and try again.',
)
@ -150,6 +150,7 @@ function App() {
/>
),
name: l.name, // name that appear in Sidebar
color: l.menuColor,
})),
)
// eslint-disable-next-line no-catch-all/no-catch-all
@ -159,7 +160,7 @@ function App() {
setError(
typeof error === 'string'
? error
: error?.errors?.[0]?.message ||
: (error?.errors?.length > 0 ? error.errors[0]?.message : null) ||
error?.message ||
'Failed to load map layers. Please check your permissions and try again.',
)

View File

@ -26,7 +26,7 @@ export class mapApi {
else return 'null'
} catch (error: any) {
console.log(error)
if (error.errors[0]?.message) throw error.errors[0].message
if (error.errors?.length > 0 && error.errors[0]?.message) throw error.errors[0].message
else throw error
}
}

View File

@ -96,6 +96,11 @@ function MapContainer({
tileServerUrl={map.tile_server_url}
tileServerAttribution={map.tile_server_attribution}
inviteApi={inviteApi}
tilesType={map.tiles_type}
maplibreStyle={map.maplibre_style}
showFullscreenControl={map.show_fullscreen_control}
zoomOffset={map.zoom_offset}
tileSize={map.tile_size}
>
{layers &&
apis &&
@ -119,6 +124,7 @@ function MapContainer({
public_edit_items={layer.public_edit_items}
listed={layer.listed}
api={apis.find((api) => api.id === layer.id)?.api}
item_default_name={layer.item_default_name}
>
<PopupView>
{layer.itemType.show_start_end && <StartEndView></StartEndView>}
@ -128,7 +134,7 @@ function MapContainer({
parameterField={
layer.itemType.custom_profile_url ? 'extended.external_profile_id' : 'id'
}
text={layer.itemType.botton_label ?? 'Profile'}
text={layer.itemType.button_label ?? 'Profile'}
target={layer.itemType.custom_profile_url ? '_blank' : '_self'}
/>
)}

View File

@ -20,19 +20,27 @@ export default defineConfig({
*/
},
plugins: [react(), tailwindcss(), tsConfigPaths()],
resolve: {
dedupe: ['react', 'react-dom', 'react-router-dom'],
},
build: {
sourcemap: true,
modulePreload: {
// Don't preload maplibre chunks - only load when actually needed
resolveDependencies: (_filename, deps) => {
return deps.filter((dep) => !dep.includes('maplibre'))
},
},
rollupOptions: {
output: {
manualChunks: (id) => {
if (id.includes('lib/src')) {
// Handle lib source (dev) or dist (prod)
if (id.includes('lib/src') || id.includes('lib/dist')) {
// Separate chunk for MapLibre components
if (id.includes('MapLibre')) {
return 'maplibre-layer'
}
return 'utopia-ui'
}
if (id.includes('node_modules')) {
if (id.includes('react')) {
if (id.includes('react') || id.includes('scheduler') || id.includes('use-sync-external-store')) {
return 'react'
}
if (id.includes('tiptap')) {
@ -41,9 +49,11 @@ export default defineConfig({
if (id.includes('leaflet')) {
return 'leaflet'
}
if (id.includes('lib/node_modules')) {
return 'utopia-ui-vendor'
} else return 'vendor'
// Separate chunk for maplibre-gl
if (id.includes('maplibre-gl')) {
return 'maplibre-gl'
}
return 'vendor'
}
},
},

View File

@ -1,12 +1,12 @@
FROM node:20-alpine as third-party-ext
FROM node:22.20.0-alpine as third-party-ext
RUN apk add python3 g++ make
WORKDIR /extensions
ADD extensions .
RUN npm install
# Move all extensions the starts with directus-extension-, using find, to the /extensions/directus folder
# Move all extensions that start with directus-extension-, using find, to the /extensions/directus folder
RUN mkdir -p ./directus
RUN cd node_modules && find . -maxdepth 1 -type d -name "directus-extension-*" -exec mv {} ../directus \;
FROM directus/directus:11.7.2
FROM directus/directus:11.9.3
# Copy third party extensions
COPY --from=third-party-ext /extensions/directus ./extensions

View File

@ -6,6 +6,7 @@ To run the backend you can simply execute
To fill in all required data execute the following commands in order:
```
cd backend
./push.sh
./seed.sh
```
@ -22,6 +23,8 @@ npx directus-sync pull \
--directus-password admin123
```
You can run `./pull.sh` to run this command and modify it via `export PROJECT=...` for a different project configuration.
## Push Data from Harddrive to Docker
To push local changes or to seed directus use the following command
@ -33,6 +36,8 @@ npx directus-sync push \
--directus-password admin123
```
You can run `./push.sh` to run this command and modify it via `export PROJECT=...` for a different project configuration.
## Seed Data for local development
In order to seed the development data, run the script `backend/seed.sh`.

View File

@ -1,3 +1,4 @@
/*
!development/
**/specs/
!.gitignore

View File

@ -1649,7 +1649,7 @@
"permissions": null,
"validation": null,
"presets": {
"role": "de2f2f91-0d78-4691-b01b-589e1345109b"
"role": "72f08162-fbd4-432e-b3f2-c8a6ecb289fc"
},
"fields": [
"first_name",
@ -2205,102 +2205,6 @@
"policy": "4d5d2bd8-7e1f-40c1-b10b-3f0ecac70877",
"_syncId": "8e8dcb5f-e3df-4d10-a44b-ac916bed0567"
},
{
"collection": "oceannomads_events",
"action": "create",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "c20b5c36-b03c-4a4d-8a86-0c5fc5805bbb"
},
{
"collection": "oceannomads_events",
"action": "delete",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "e61f5cdf-33e2-46db-828b-aab05c30dd32"
},
{
"collection": "oceannomads_events",
"action": "read",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "c86a309a-d4ec-4135-bff4-238825ea7053"
},
{
"collection": "oceannomads_events",
"action": "update",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "d03d2704-3311-4dc1-bb94-b542c89f94b4"
},
{
"collection": "oceannomads_profiles",
"action": "create",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "5a2ba99b-bd06-4a47-8fc1-3919001d1c4a"
},
{
"collection": "oceannomads_profiles",
"action": "delete",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "47783f7d-cb09-4dbd-a2ad-cf26c3c02192"
},
{
"collection": "oceannomads_profiles",
"action": "read",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "3f0621ff-34ba-4f6c-9274-b5fb1ccf8c3a"
},
{
"collection": "oceannomads_profiles",
"action": "update",
"permissions": null,
"validation": null,
"presets": null,
"fields": [
"*"
],
"policy": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c",
"_syncId": "dc87ced0-4465-4e68-8732-97d73b0d2de4"
},
{
"collection": "attestations_directus_users",
"action": "create",

View File

@ -23,20 +23,7 @@
"enforce_tfa": false,
"admin_access": false,
"app_access": true,
"roles": [
{
"role": null,
"sort": 1
},
{
"role": null,
"sort": 1
},
{
"role": null,
"sort": null
}
],
"roles": [],
"_syncId": "_sync_default_public_policy"
},
{
@ -79,12 +66,7 @@
"enforce_tfa": false,
"admin_access": false,
"app_access": false,
"roles": [
{
"role": null,
"sort": null
}
],
"roles": [],
"_syncId": "b0eb656b-96e5-4a30-a083-6ef8141e6a4c"
},
{

View File

@ -60,6 +60,8 @@
"public_registration_role": null,
"public_registration_email_filter": null,
"visual_editor_urls": null,
"accepted_terms": true,
"project_id": "0199aa52-4dd7-7293-984a-f2af93b5f8fd",
"_syncId": "55f04445-0c26-4201-ab9c-d6e0fbadf6bf"
}
]

View File

@ -0,0 +1,49 @@
{
"collection": "Themes",
"meta": {
"insert_order": 1,
"create": true,
"update": true,
"delete": true,
"preserve_ids": true,
"ignore_on_update": []
},
"data": [
{
"_sync_id": "theme-light",
"theme": "light"
},
{
"_sync_id": "theme-dark",
"theme": "dark"
},
{
"_sync_id": "theme-valentine",
"theme": "valentine"
},
{
"_sync_id": "theme-retro",
"theme": "retro"
},
{
"_sync_id": "theme-aqua",
"theme": "aqua"
},
{
"_sync_id": "theme-cyberpunk",
"theme": "cyberpunk"
},
{
"_sync_id": "theme-caramellatte",
"theme": "caramellatte"
},
{
"_sync_id": "theme-abyss",
"theme": "abyss"
},
{
"_sync_id": "theme-silk",
"theme": "silk"
}
]
}

View File

@ -17,16 +17,6 @@
"title": "Utopia Logo",
"tags": [],
"description": "Utopia Logo"
},
{
"_sync_id": "vessel-svg",
"_file_path": "./files/vessel.svg",
"storage": "local",
"folder": "27b2a288-d50a-48b7-88cd-35945503277b",
"filename_download": "vessel.svg",
"title": "Vessel SVG",
"tags": [],
"description": "Vessel SVG"
}
]
}

View File

@ -1,19 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="666px" height="541px" viewBox="0 0 6660 5410" preserveAspectRatio="xMidYMid meet">
<g id="layer101" fill="#000000" stroke="none">
</g>
<g id="layer102" fill="#182d45" stroke="none">
<path d="M2600 5360 c-58 -5 -242 -8 -410 -8 -168 0 -316 -2 -330 -5 -14 -4 -81 -11 -150 -17 -125 -11 -369 -43 -445 -60 -22 -5 -82 -13 -134 -20 -172 -20 -424 -141 -607 -291 -159 -131 -215 -184 -288 -270 -105 -126 -138 -192 -176 -357 -26 -112 -29 -139 -20 -172 13 -48 30 -66 78 -81 55 -17 2639 -31 4715 -26 2008 5 1780 -11 1785 125 7 204 -81 382 -303 611 -119 123 -228 214 -315 263 -66 38 -193 81 -375 128 -185 49 -240 53 -863 69 -204 5 -376 13 -382 16 -5 3 -128 10 -272 16 -145 5 -375 14 -513 19 -137 5 -302 16 -365 24 -285 36 -483 47 -630 36z"/>
<path d="M1025 3861 c-75 -10 -141 -39 -180 -80 -27 -30 -30 -38 -29 -99 0 -81 21 -130 253 -597 171 -344 251 -490 353 -643 87 -129 163 -274 224 -422 58 -144 73 -173 145 -290 94 -154 161 -204 215 -159 40 33 45 71 44 319 0 132 4 301 9 377 12 163 14 329 13 868 -1 230 3 440 9 495 14 124 2 166 -56 195 -34 17 -70 21 -250 27 -348 13 -686 17 -750 9z"/>
<path d="M4653 3856 c-31 -9 -78 -49 -96 -81 -9 -15 -12 -174 -13 -585 0 -574 -17 -1381 -29 -1400 -12 -20 16 -602 30 -637 8 -18 29 -42 47 -53 67 -40 129 -17 169 63 20 40 34 63 132 217 63 100 173 307 323 610 75 151 151 297 170 323 19 26 44 71 55 100 11 29 47 102 79 162 32 61 79 157 103 215 83 192 112 255 134 290 12 19 33 64 46 100 14 36 53 130 87 210 34 80 73 171 87 203 18 44 23 68 18 97 -8 48 -20 63 -70 91 -91 51 -93 51 -283 60 -103 5 -227 13 -277 19 -106 12 -673 9 -712 -4z"/>
<path d="M2412 3831 c-137 -6 -145 -7 -164 -31 -15 -18 -19 -34 -15 -60 27 -171 49 -238 144 -440 30 -63 66 -148 79 -188 13 -39 42 -109 65 -154 23 -45 66 -139 95 -208 59 -136 131 -270 259 -480 45 -74 116 -196 158 -270 41 -74 123 -207 182 -295 59 -88 123 -191 142 -230 20 -38 79 -137 133 -220 53 -82 115 -182 138 -222 23 -40 75 -116 115 -170 129 -174 268 -394 364 -573 92 -173 150 -250 189 -250 27 0 91 72 103 115 17 63 14 520 -5 683 -14 122 -15 175 -3 492 7 195 14 661 15 1035 0 374 4 802 8 951 8 312 4 346 -52 395 -20 17 -50 37 -67 45 -35 14 -457 34 -739 34 -94 0 -220 4 -280 10 -324 29 -650 41 -864 31z"/>
</g>
<g id="layer103" fill="#37949a" stroke="none">
<path d="M1950 5193 c-8 -1 -96 -9 -195 -18 -163 -14 -462 -52 -475 -60 -3 -2 -50 -8 -105 -15 -115 -14 -190 -40 -324 -112 -115 -62 -204 -123 -280 -192 -31 -29 -80 -69 -108 -90 -29 -21 -53 -41 -53 -44 0 -4 -26 -36 -59 -72 -64 -72 -115 -156 -126 -210 -4 -19 -20 -68 -36 -108 -16 -40 -29 -74 -29 -75 0 -1 1317 -3 2928 -4 1610 -1 3032 -5 3161 -8 l234 -7 -6 34 c-36 190 -116 317 -339 533 -172 168 -263 221 -433 251 -43 7 -103 21 -132 29 -129 38 -291 50 -783 60 -146 3 -346 9 -445 15 -336 18 -640 30 -755 30 -64 0 -170 7 -235 15 -342 42 -444 48 -920 49 -259 1 -478 0 -485 -1z"/>
</g>
<g id="layer104" fill="#f9d400" stroke="none">
<path d="M3515 379 c-97 -43 -174 -69 -201 -69 -13 0 -58 17 -99 37 -60 30 -79 35 -93 26 -28 -18 -20 -76 21 -137 41 -61 82 -95 145 -119 60 -24 101 -21 158 9 155 84 178 94 214 94 24 0 92 -22 174 -55 148 -60 187 -67 210 -34 22 32 6 60 -55 95 -30 16 -83 52 -119 78 -149 111 -232 129 -355 75z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -16,6 +16,10 @@
{
"_sync_id": "b0c52d6e-b3d2-4e3b-89e2-065be324e27b",
"hideInputLabel": false
},
{
"_sync_id": "6d18b616-6f4f-4987-9860-681b88bdc068",
"hideInputLabel": false
}
]
}

View File

@ -13,7 +13,7 @@
"_sync_id": "item-places-1",
"name": "Welcome to Utopia Map",
"subname" : "The opensource collaborative mapping plattform",
"text": "Check out our [GitHub](https://github.com/utopia-os/utopia-map)!",
"text": "The Utopia Map is a flexible collaborative app for decentralized coordination and real-life networking that can be adapted to the specific requirements of different networks. Its central element is the interactive geographical map, where users can add and manage **Items** in predefined **Layers**. \n\nUtopia Map is made for networks and initiatives that aim to connect people in real life. By providing a custom instance of Utopia Map, each network can grow and coordinate its ecosystem effectively while encouraging real-world interactions and collaborations.\n\n## Key Features\n- **Interactive Map**: The core feature is an intuitive geographical map where users can add, edit, and manage items like members, activities, and resources. Each map instance has its own identity, users, and unique configuration.\n- **Customizable Layers**: Items are organized into predefined Layers, each with specific icons, colors, texts, and Map Markers. This ensures clarity and relevance for different networks.\n- **Dynamic Map Markers**: Geographic position of item are indicated on the map by adaptive and customizable Map Markers\n- **Popups**: Clicking a Map Marker reveals a Popup — a compact preview of the Item with its most relevant information. Define custom Popups for each of your Layers.\n- **Profiles**: Each Item has a dedicated Profile that showcases all its associated data, making it easier to explore and manage. Define custom profiles for each of your Layers.\n\nCheck out our [GitHub](https://github.com/utopia-os/utopia-map)!",
"position": {
"type": "Point",
"coordinates": [
@ -21,13 +21,13 @@
50.51565268622562
]
},
"image": "utopia-logo",
"layer" : "layer-places"
},
{
"_sync_id": "item-event-1",
"name": "Some Event",
"subname" : "The opensource collaborative mapping plattform",
"text": "Check out our [GitHub](https://github.com/utopia-os/utopia-map)!",
"text": "This is an example event. Events are temporary items that disappear once the end date has passed.",
"position": {
"type": "Point",
"coordinates": [
@ -40,9 +40,9 @@
"end": "2027-06-25T12:00:00"
},
{
"_sync_id": "item-nomad-location-1",
"name": "Anton Tranelis",
"text": "bla blab ...",
"_sync_id": "item-user-1",
"name": "Admin User",
"text": "This is the personal profile of the admin user. Every user can place his personal profile on the map.",
"position": {
"type": "Point",
"coordinates": [
@ -50,46 +50,134 @@
51.41565268622562
]
},
"layer" : "layer-nomads_location"
"layer" : "layer-people"
},
{
"_sync_id": "item-nomad-base-1",
"name": "Anton Tranelis",
"text": "bla blab ...",
"_sync_id": "item-places-2",
"name": "Community Garden Berlin",
"subname": "Organic vegetables and sustainable farming",
"text": "A thriving community garden in Berlin where neighbors grow organic vegetables together. We focus on #sustainability #community #gardening #organic practices. Join us for weekly workshops on #permaculture and #composting!",
"position": {
"type": "Point",
"coordinates": [
9.67625824315172,
48.41565268622562
13.404954,
52.520008
]
},
"layer" : "layer-nomads_base"
"layer" : "layer-places"
},
{
"_sync_id": "item-vessel-1",
"name": "Vessel XY",
"text": "shipping the sea",
"_sync_id": "item-event-2",
"name": "Tech Meetup Munich",
"text": "Monthly #technology #networking event for developers and tech enthusiasts. Topics include #javascript #opensource #webdev and #innovation. Great for #learning and meeting like-minded people!",
"position": {
"type": "Point",
"coordinates": [
-2.67625824315172,
48.61565268622562
11.576124,
48.137154
]
},
"layer" : "layer-vessels"
"layer" : "layer-events",
"start": "2025-12-15T18:00:00",
"end": "2025-12-15T21:00:00"
},
{
"_sync_id": "item-basecamp-1",
"name": "Basecamp XY",
"text": "come and join our camp",
{
"_sync_id": "item-people-2",
"name": "Jane Developer",
"text": "Full-stack developer passionate about #opensource projects and #javascript frameworks. Interested in #sustainability tech and #community building. Always happy to collaborate on #innovation projects!",
"position": {
"type": "Point",
"coordinates": [
1.6007423400878908,
50.184428095190555
2.349014,
48.864716
]
},
"layer" : "layer-basecamps"
"layer" : "layer-people"
},
{
"_sync_id": "item-places-3",
"name": "Café Collaboration London",
"subname": "Co-working space and coffee shop",
"text": "A vibrant co-working café in London perfect for #networking and #collaboration. Features excellent coffee, fast wifi, and a #community of freelancers and entrepreneurs. Regular #events and workshops on #business and #creativity.",
"position": {
"type": "Point",
"coordinates": [
-0.127758,
51.507351
]
},
"layer" : "layer-places"
},
{
"_sync_id": "item-event-3",
"name": "Sustainability Workshop NYC",
"text": "Learn about #sustainability practices and #environmental solutions in this hands-on workshop. Topics include #recycling #renewable energy and #green living. Perfect for #activists and #eco enthusiasts!",
"position": {
"type": "Point",
"coordinates": [
-74.005941,
40.712784
]
},
"layer" : "layer-events",
"start": "2025-11-20T14:00:00",
"end": "2025-11-20T17:00:00"
},
{
"_sync_id": "item-people-3",
"name": "Alex Entrepreneur",
"text": "Serial entrepreneur focused on #sustainability and #innovation. Building #community-driven solutions for #environmental challenges. Always looking for #collaboration opportunities in #greentech!",
"position": {
"type": "Point",
"coordinates": [
-122.419416,
37.774929
]
},
"layer" : "layer-people"
},
{
"_sync_id": "item-places-4",
"name": "Makerspace Tokyo",
"subname": "Innovation hub for creators",
"text": "State-of-the-art makerspace in Tokyo with 3D printers, laser cutters, and electronics lab. Perfect for #makers #innovation #prototyping and #learning. Join our #community of inventors and #entrepreneurs!",
"position": {
"type": "Point",
"coordinates": [
139.691706,
35.689487
]
},
"layer" : "layer-places"
},
{
"_sync_id": "item-event-4",
"name": "Open Source Conference",
"text": "Annual conference celebrating #opensource software and #collaboration. Features talks on #javascript #python #webdev and #community building. Great for #developers #learning and #networking!",
"position": {
"type": "Point",
"coordinates": [
-0.118092,
51.509865
]
},
"layer" : "layer-events",
"start": "2025-10-10T09:00:00",
"end": "2025-10-12T18:00:00"
},
{
"_sync_id": "item-places-5",
"name": "Test & Special Characters!",
"subname": "Edge case for search testing",
"text": "This item tests special characters: @#$%^&*()_+ and unicode: café naïve résumé. Also tests very long content that might affect search performance and display. Contains #testing #edgecase #unicode #special-chars tags.",
"position": {
"type": "Point",
"coordinates": [
4.902257,
52.367573
]
},
"layer" : "layer-places"
}
]
}

View File

@ -12,72 +12,13 @@
{
"_sync_id": "layer-places",
"name": "Places",
"itemType": "type-simple",
"itemType": "type-text-gallery",
"userProfileLayer": false,
"indexIcon": "map-pin-outline",
"menuColor": "#2ECDA7",
"menuIcon": "point-solid",
"menuColor": "#008e5b",
"menuText": "Add new Place",
"markerIcon" : "marker-point",
"markerShape" : "circle",
"markerDefaultColor2": null,
"onlyOnePerOwner": false,
"index_plus_button": true,
"public_edit_items": false,
"listed": true,
"item_presets": null,
"sort": 1
},
{
"_sync_id": "layer-events",
"name": "Events",
"itemType": "type-event",
"userProfileLayer": false,
"indexIcon": "calendar-outline",
"menuColor": "#6644FF",
"menuIcon": "calendar-solid",
"menuText": "Add new Event",
"markerIcon" : "marker-calendar",
"markerShape" : "square",
"markerDefaultColor2": null,
"onlyOnePerOwner": false,
"index_plus_button": true,
"public_edit_items": false,
"listed": true,
"item_presets": null,
"sort": 5
},
{
"_sync_id": "layer-nomads_location",
"name": "Nomads Location",
"itemType": "type-ON_nomads_location",
"userProfileLayer": false,
"indexIcon": "users-outline",
"menuColor": "#18222F",
"menuIcon": "user-solid",
"menuText": "Share your Location",
"markerIcon" : "marker-user",
"markerShape" : "square",
"markerDefaultColor2": null,
"onlyOnePerOwner": true,
"index_plus_button": true,
"public_edit_items": false,
"listed": true,
"item_presets": null,
"sort": 1
},
{
"_sync_id": "layer-nomads_base",
"name": "Nomads Base",
"itemType": "type-ON_nomads_location",
"userProfileLayer": false,
"indexIcon": "house-outline",
"menuColor": "#B05463",
"menuIcon" : "house-solid",
"menuText": "Share a new Home Base",
"markerIcon" : "marker-house",
"markerShape" : "square",
"markerDefaultColor2": null,
"onlyOnePerOwner": true,
"index_plus_button": true,
"public_edit_items": false,
"listed": true,
@ -85,42 +26,37 @@
"sort": 2
},
{
"_sync_id": "layer-vessels",
"name": "Vessels",
"itemType": "type-text-gallery",
"_sync_id": "layer-events",
"name": "Events",
"itemType": "type-event",
"userProfileLayer": false,
"indexIcon": "boat-outline",
"menuColor": "#19898F",
"menuIcon" : "boat-solid",
"menuText": "Add a new Vessel",
"markerIcon" : "marker-boat",
"menuColor": "#c4037d",
"menuText": "Add new Event",
"markerIcon" : "marker-calendar",
"markerShape" : "square",
"markerDefaultColor2": null,
"onlyOnePerOwner": true,
"index_plus_button": true,
"public_edit_items": false,
"listed": true,
"item_presets": null,
"sort": 3
},
{
"_sync_id": "layer-basecamps",
"name": "Basecamps",
"itemType": "type-text-gallery",
"userProfileLayer": false,
"indexIcon": "camp-solid",
"menuColor": "#FFA439",
"menuIcon" : "camp-solid",
"menuText": "Add a new Basecamp",
"markerIcon" : "marker-camp",
"_sync_id": "layer-people",
"name": "People",
"itemType": "type-user-text-gallery",
"userProfileLayer": true,
"menuColor": "#e87520",
"menuText": "Add a new Vessel",
"markerIcon" : "marker-user",
"markerShape" : "square",
"markerDefaultColor2": null,
"onlyOnePerOwner": true,
"index_plus_button": true,
"public_edit_items": false,
"listed": true,
"item_presets": null,
"sort": 4
"sort": 1
}
]
}

View File

@ -15,23 +15,13 @@
"maps_id": "map-local-development"
},
{
"_sync_id": "layer-nomads-location-map-local-development",
"layers_id": "layer-nomads_location",
"_sync_id": "layer-people-map-local-development",
"layers_id": "layer-people",
"maps_id": "map-local-development"
},
{
"_sync_id": "layer-nomads-base-map-local-development",
"layers_id": "layer-nomads_base",
"maps_id": "map-local-development"
},
{
"_sync_id": "layer-vessel-map-local-development",
"layers_id": "layer-vessels",
"maps_id": "map-local-development"
},
{
"_sync_id": "layer-basecamps-map-local-development",
"layers_id": "layer-basecamps",
"_sync_id": "layer-places-map-local-development",
"layers_id": "layer-places",
"maps_id": "map-local-development"
}
]

View File

@ -13,7 +13,7 @@
"_sync_id": "map-local-development",
"name": "Local Development",
"url": "http://local.development",
"logo": "vessel-svg",
"logo": "utopia-logo",
"zoom": 6,
"own_tag_space": true,
"center": {

View File

@ -133,7 +133,9 @@
"_sync_id": "marker-point",
"id": "point",
"size": "12.00000",
"image": "point-solid"
"image": "point-solid",
"image_outline": "map-pin-outline",
"size_outline": "18.00000"
},
{
"_sync_id": "marker-puzzle",

View File

@ -24,6 +24,14 @@
"hideWhenEmpty": true,
"showMarkdownHint": true,
"size": "full"
},
{
"_sync_id": "c960bbfc-5d98-4f6d-ae44-7a2b63d3359b",
"dataField": "text",
"heading": null,
"hideWhenEmpty": true,
"showMarkdownHint": true,
"size": "full"
}
]
}

View File

@ -29,6 +29,7 @@
"user_updated": null,
"date_updated": null,
"template": "flex",
"show_start_end_input": true,
"show_text": true,
"show_profile_button" : true,
"show_start_end" : true
@ -65,6 +66,22 @@
"show_name_input" : true,
"show_header_view_in_form" : false,
"small_form_edit" : false
},
{
"_sync_id": "type-user-text-gallery",
"name": "user:text+gallery",
"user_created": null,
"date_created": "2025-01-01T00:00:00.000Z",
"user_updated": null,
"date_updated": null,
"template": "flex",
"show_text": true,
"show_profile_button" : true,
"show_start_end" : false,
"show_text_input" : false,
"show_name_input" : false,
"show_header_view_in_form" : false,
"small_form_edit" : false
}
]
}

View File

@ -1,28 +0,0 @@
{
"collection": "oceannomads_events",
"meta": {
"accountability": "all",
"archive_app_filter": true,
"archive_field": null,
"archive_value": null,
"collapse": "open",
"collection": "oceannomads_events",
"color": null,
"display_template": null,
"group": null,
"hidden": false,
"icon": null,
"item_duplication_fields": null,
"note": null,
"preview_url": null,
"singleton": false,
"sort": 23,
"sort_field": null,
"translations": null,
"unarchive_value": null,
"versioning": false
},
"schema": {
"name": "oceannomads_events"
}
}

View File

@ -1,28 +0,0 @@
{
"collection": "oceannomads_profiles",
"meta": {
"accountability": "all",
"archive_app_filter": true,
"archive_field": null,
"archive_value": null,
"collapse": "open",
"collection": "oceannomads_profiles",
"color": null,
"display_template": null,
"group": null,
"hidden": false,
"icon": null,
"item_duplication_fields": null,
"note": null,
"preview_url": null,
"singleton": false,
"sort": 24,
"sort_field": null,
"translations": null,
"unarchive_value": null,
"versioning": false
},
"schema": {
"name": "oceannomads_profiles"
}
}

View File

@ -1,13 +1,13 @@
{
"collection": "oceannomads_profiles",
"field": "email",
"collection": "layers",
"field": "item_default_name",
"type": "string",
"meta": {
"collection": "oceannomads_profiles",
"collection": "layers",
"conditions": null,
"display": null,
"display_options": null,
"field": "email",
"field": "item_default_name",
"group": null,
"hidden": false,
"interface": "input",
@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 2,
"sort": 16,
"special": null,
"translations": null,
"validation": null,
@ -23,10 +23,10 @@
"width": "half"
},
"schema": {
"name": "email",
"table": "oceannomads_profiles",
"name": "item_default_name",
"table": "layers",
"data_type": "character varying",
"default_value": null,
"default_value": "item",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,

View File

@ -18,7 +18,7 @@
},
"readonly": false,
"required": false,
"sort": 16,
"sort": 17,
"special": [
"cast-json"
],

View File

@ -15,7 +15,7 @@
"options": {},
"readonly": false,
"required": false,
"sort": 17,
"sort": 18,
"special": [
"m2m"
],

View File

@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 12,
"sort": 11,
"special": [
"alias",
"no-data",

View File

@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 15,
"sort": 14,
"special": [
"alias",
"no-data",

View File

@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 13,
"sort": 12,
"special": null,
"translations": null,
"validation": null,

View File

@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 10,
"sort": 9,
"special": [
"m2o"
],

View File

@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 11,
"sort": 10,
"special": [
"cast-boolean"
],

View File

@ -26,7 +26,7 @@
},
"readonly": false,
"required": false,
"sort": 14,
"sort": 13,
"special": [
"cast-json"
],

View File

@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 17,
"sort": 16,
"special": [
"cast-boolean"
],

View File

@ -0,0 +1,46 @@
{
"collection": "maps",
"field": "maplibre",
"type": "alias",
"meta": {
"collection": "maps",
"conditions": [
{
"hidden": false,
"name": "Show when maplibre tiles selected",
"options": null,
"rule": {
"_and": [
{
"tiles_type": {
"_eq": "maplibre"
}
}
]
}
}
],
"display": null,
"display_options": null,
"field": "maplibre",
"group": "tile_server",
"hidden": true,
"interface": "group-detail",
"note": "Configuration for MapLibre GL vector tiles",
"options": {
"start": "open"
},
"readonly": false,
"required": false,
"sort": 3,
"special": [
"alias",
"no-data",
"group"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
}
}

View File

@ -1,21 +1,23 @@
{
"collection": "oceannomads_profiles",
"field": "avatar_url",
"collection": "maps",
"field": "maplibre_style",
"type": "string",
"meta": {
"collection": "oceannomads_profiles",
"collection": "maps",
"conditions": null,
"display": null,
"display_options": null,
"field": "avatar_url",
"group": null,
"field": "maplibre_style",
"group": "maplibre",
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"note": "MapLibre style URL (default: OpenFreeMap Liberty style)",
"options": {
"placeholder": "https://tiles.openfreemap.org/styles/liberty"
},
"readonly": false,
"required": false,
"sort": 8,
"sort": 1,
"special": null,
"translations": null,
"validation": null,
@ -23,8 +25,8 @@
"width": "full"
},
"schema": {
"name": "avatar_url",
"table": "oceannomads_profiles",
"name": "maplibre_style",
"table": "maps",
"data_type": "character varying",
"default_value": null,
"max_length": 255,

View File

@ -0,0 +1,46 @@
{
"collection": "maps",
"field": "raster_tiles",
"type": "alias",
"meta": {
"collection": "maps",
"conditions": [
{
"hidden": false,
"name": "Show when raster tiles selected",
"options": null,
"rule": {
"_and": [
{
"tiles_type": {
"_eq": "raster"
}
}
]
}
}
],
"display": null,
"display_options": null,
"field": "raster_tiles",
"group": "tile_server",
"hidden": true,
"interface": "group-detail",
"note": "Configuration for raster tile layers",
"options": {
"start": "open"
},
"readonly": false,
"required": false,
"sort": 2,
"special": [
"alias",
"no-data",
"group"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
}
}

View File

@ -1,32 +1,34 @@
{
"collection": "oceannomads_events",
"field": "end",
"type": "dateTime",
"collection": "maps",
"field": "show_fullscreen_control",
"type": "boolean",
"meta": {
"collection": "oceannomads_events",
"collection": "maps",
"conditions": null,
"display": null,
"display_options": null,
"field": "end",
"group": null,
"field": "show_fullscreen_control",
"group": "Controls",
"hidden": false,
"interface": "datetime",
"interface": "boolean",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 6,
"special": null,
"special": [
"cast-boolean"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
"width": "half"
},
"schema": {
"name": "end",
"table": "oceannomads_events",
"data_type": "timestamp without time zone",
"default_value": null,
"name": "show_fullscreen_control",
"table": "maps",
"data_type": "boolean",
"default_value": false,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,

View File

@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 18,
"sort": 17,
"special": [
"cast-boolean"
],

View File

@ -17,7 +17,7 @@
},
"readonly": false,
"required": false,
"sort": 16,
"sort": 15,
"special": [
"alias",
"no-data",

View File

@ -11,11 +11,13 @@
"group": "tile_server",
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"note": "Attribution text for raster tiles",
"options": {
"placeholder": "&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a>"
},
"readonly": false,
"required": false,
"sort": 2,
"sort": 4,
"special": null,
"translations": null,
"validation": null,

View File

@ -8,11 +8,13 @@
"display": null,
"display_options": null,
"field": "tile_server_url",
"group": "tile_server",
"group": "raster_tiles",
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"note": "Raster tile server URL template (e.g., https://tile.osmand.net/hd/{z}/{x}/{y}.png)",
"options": {
"placeholder": "https://tile.osmand.net/hd/{z}/{x}/{y}.png"
},
"readonly": false,
"required": false,
"sort": 1,

View File

@ -1,21 +1,21 @@
{
"collection": "oceannomads_profiles",
"field": "last_name",
"type": "string",
"collection": "maps",
"field": "tile_size",
"type": "integer",
"meta": {
"collection": "oceannomads_profiles",
"collection": "maps",
"conditions": null,
"display": null,
"display_options": null,
"field": "last_name",
"group": null,
"field": "tile_size",
"group": "raster_tiles",
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 6,
"sort": 2,
"special": null,
"translations": null,
"validation": null,
@ -23,13 +23,13 @@
"width": "half"
},
"schema": {
"name": "last_name",
"table": "oceannomads_profiles",
"data_type": "character varying",
"default_value": null,
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"name": "tile_size",
"table": "maps",
"data_type": "integer",
"default_value": 256,
"max_length": null,
"numeric_precision": 32,
"numeric_scale": 0,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,

View File

@ -1,21 +1,32 @@
{
"collection": "oceannomads_events",
"field": "creator_email",
"collection": "maps",
"field": "tiles_type",
"type": "string",
"meta": {
"collection": "oceannomads_events",
"collection": "maps",
"conditions": null,
"display": null,
"display_options": null,
"field": "creator_email",
"group": null,
"field": "tiles_type",
"group": "tile_server",
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"interface": "select-dropdown",
"note": "Choose between raster tiles or vector tiles (MapLibre GL)",
"options": {
"choices": [
{
"text": "Raster Tiles",
"value": "raster"
},
{
"text": "Vector Tiles (MapLibre GL)",
"value": "maplibre"
}
]
},
"readonly": false,
"required": false,
"sort": 9,
"sort": 1,
"special": null,
"translations": null,
"validation": null,
@ -23,10 +34,10 @@
"width": "full"
},
"schema": {
"name": "creator_email",
"table": "oceannomads_events",
"name": "tiles_type",
"table": "maps",
"data_type": "character varying",
"default_value": null,
"default_value": "raster",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,

View File

@ -1,35 +1,37 @@
{
"collection": "oceannomads_events",
"field": "text",
"type": "text",
"collection": "maps",
"field": "zoom_offset",
"type": "integer",
"meta": {
"collection": "oceannomads_events",
"collection": "maps",
"conditions": null,
"display": null,
"display_options": null,
"field": "text",
"group": null,
"field": "zoom_offset",
"group": "raster_tiles",
"hidden": false,
"interface": "input-multiline",
"interface": "input",
"note": null,
"options": null,
"options": {
"min": 0
},
"readonly": false,
"required": false,
"sort": 7,
"sort": 3,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
"width": "half"
},
"schema": {
"name": "text",
"table": "oceannomads_events",
"data_type": "text",
"default_value": null,
"name": "zoom_offset",
"table": "maps",
"data_type": "integer",
"default_value": -1,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"numeric_precision": 32,
"numeric_scale": 0,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,

View File

@ -1,47 +0,0 @@
{
"collection": "oceannomads_events",
"field": "date_created",
"type": "timestamp",
"meta": {
"collection": "oceannomads_events",
"conditions": null,
"display": "datetime",
"display_options": {
"relative": true
},
"field": "date_created",
"group": null,
"hidden": true,
"interface": "datetime",
"note": null,
"options": null,
"readonly": true,
"required": false,
"sort": 2,
"special": [
"date-created"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "date_created",
"table": "oceannomads_events",
"data_type": "timestamp with time zone",
"default_value": null,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,47 +0,0 @@
{
"collection": "oceannomads_events",
"field": "date_updated",
"type": "timestamp",
"meta": {
"collection": "oceannomads_events",
"conditions": null,
"display": "datetime",
"display_options": {
"relative": true
},
"field": "date_updated",
"group": null,
"hidden": true,
"interface": "datetime",
"note": null,
"options": null,
"readonly": true,
"required": false,
"sort": 3,
"special": [
"date-updated"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "date_updated",
"table": "oceannomads_events",
"data_type": "timestamp with time zone",
"default_value": null,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,43 +0,0 @@
{
"collection": "oceannomads_events",
"field": "id",
"type": "string",
"meta": {
"collection": "oceannomads_events",
"conditions": null,
"display": null,
"display_options": null,
"field": "id",
"group": null,
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 1,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
},
"schema": {
"name": "id",
"table": "oceannomads_events",
"data_type": "character varying",
"default_value": null,
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": false,
"is_unique": true,
"is_indexed": false,
"is_primary_key": true,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,47 +0,0 @@
{
"collection": "oceannomads_profiles",
"field": "date_created",
"type": "timestamp",
"meta": {
"collection": "oceannomads_profiles",
"conditions": null,
"display": "datetime",
"display_options": {
"relative": true
},
"field": "date_created",
"group": null,
"hidden": true,
"interface": "datetime",
"note": null,
"options": null,
"readonly": true,
"required": false,
"sort": 3,
"special": [
"date-created"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "date_created",
"table": "oceannomads_profiles",
"data_type": "timestamp with time zone",
"default_value": null,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,47 +0,0 @@
{
"collection": "oceannomads_profiles",
"field": "date_updated",
"type": "timestamp",
"meta": {
"collection": "oceannomads_profiles",
"conditions": null,
"display": "datetime",
"display_options": {
"relative": true
},
"field": "date_updated",
"group": null,
"hidden": true,
"interface": "datetime",
"note": null,
"options": null,
"readonly": true,
"required": false,
"sort": 4,
"special": [
"date-updated"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "date_updated",
"table": "oceannomads_profiles",
"data_type": "timestamp with time zone",
"default_value": null,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,43 +0,0 @@
{
"collection": "oceannomads_profiles",
"field": "id",
"type": "string",
"meta": {
"collection": "oceannomads_profiles",
"conditions": null,
"display": null,
"display_options": null,
"field": "id",
"group": null,
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 1,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "id",
"table": "oceannomads_profiles",
"data_type": "character varying",
"default_value": null,
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": false,
"is_unique": true,
"is_indexed": false,
"is_primary_key": true,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,43 +0,0 @@
{
"collection": "oceannomads_profiles",
"field": "location",
"type": "string",
"meta": {
"collection": "oceannomads_profiles",
"conditions": null,
"display": null,
"display_options": null,
"field": "location",
"group": null,
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 7,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
},
"schema": {
"name": "location",
"table": "oceannomads_profiles",
"data_type": "character varying",
"default_value": null,
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,21 +1,34 @@
{
"collection": "oceannomads_events",
"field": "location",
"collection": "relations",
"field": "direction",
"type": "string",
"meta": {
"collection": "oceannomads_events",
"collection": "relations",
"conditions": null,
"display": null,
"display_options": null,
"field": "location",
"field": "direction",
"group": null,
"hidden": false,
"interface": "input",
"interface": "select-dropdown",
"note": null,
"options": null,
"options": {
"choices": [
{
"icon": "arrow_forward",
"text": "outgoing",
"value": "outgoing"
},
{
"icon": "arrow_back",
"text": "ingoing",
"value": "ingoing"
}
]
},
"readonly": false,
"required": false,
"sort": 8,
"sort": 4,
"special": null,
"translations": null,
"validation": null,
@ -23,10 +36,10 @@
"width": "full"
},
"schema": {
"name": "location",
"table": "oceannomads_events",
"name": "direction",
"table": "relations",
"data_type": "character varying",
"default_value": null,
"default_value": "outgoing",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,

View File

@ -1,13 +1,13 @@
{
"collection": "oceannomads_events",
"field": "title",
"collection": "relations",
"field": "heading",
"type": "string",
"meta": {
"collection": "oceannomads_events",
"collection": "relations",
"conditions": null,
"display": null,
"display_options": null,
"field": "title",
"field": "heading",
"group": null,
"hidden": false,
"interface": "input",
@ -15,7 +15,7 @@
"options": null,
"readonly": false,
"required": false,
"sort": 4,
"sort": 3,
"special": null,
"translations": null,
"validation": null,
@ -23,10 +23,10 @@
"width": "full"
},
"schema": {
"name": "title",
"table": "oceannomads_events",
"name": "heading",
"table": "relations",
"data_type": "character varying",
"default_value": null,
"default_value": "Relations",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,

View File

@ -1,32 +1,34 @@
{
"collection": "oceannomads_events",
"field": "start",
"type": "dateTime",
"collection": "relations",
"field": "hideWhenEmpty",
"type": "boolean",
"meta": {
"collection": "oceannomads_events",
"collection": "relations",
"conditions": null,
"display": null,
"display_options": null,
"field": "start",
"field": "hideWhenEmpty",
"group": null,
"hidden": false,
"interface": "datetime",
"interface": "boolean",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 5,
"special": null,
"special": [
"cast-boolean"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
},
"schema": {
"name": "start",
"table": "oceannomads_events",
"data_type": "timestamp without time zone",
"default_value": null,
"name": "hideWhenEmpty",
"table": "relations",
"data_type": "boolean",
"default_value": true,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,

View File

@ -0,0 +1,64 @@
{
"collection": "types",
"field": "Flex",
"type": "alias",
"meta": {
"collection": "types",
"conditions": [
{
"name": "Flex-Template",
"options": {
"start": "open"
},
"readonly": false,
"required": true,
"rule": {
"_and": [
{
"template": {
"_eq": "flex"
}
}
]
}
},
{
"hidden": true,
"name": "Not Flex Template",
"options": {
"start": "closed"
},
"readonly": true,
"rule": {
"_and": [
{
"template": {
"_neq": "flex"
}
}
]
}
}
],
"display": null,
"display_options": null,
"field": "Flex",
"group": "Profile",
"hidden": false,
"interface": "group-raw",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 2,
"special": [
"alias",
"no-data",
"group"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
}
}

View File

@ -0,0 +1,32 @@
{
"collection": "types",
"field": "Header",
"type": "alias",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "Header",
"group": null,
"hidden": false,
"interface": "group-detail",
"note": null,
"options": {
"headerIcon": "credit_card",
"start": "closed"
},
"readonly": false,
"required": false,
"sort": 7,
"special": [
"alias",
"no-data",
"group"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
}
}

View File

@ -12,10 +12,13 @@
"hidden": false,
"interface": "group-detail",
"note": null,
"options": null,
"options": {
"headerIcon": "lab_profile",
"start": "closed"
},
"readonly": false,
"required": false,
"sort": 9,
"sort": 10,
"special": [
"alias",
"no-data",

View File

@ -4,16 +4,51 @@
"type": "alias",
"meta": {
"collection": "types",
"conditions": null,
"conditions": [
{
"name": "Tabs Template",
"options": {
"start": "open"
},
"readonly": false,
"rule": {
"_and": [
{
"template": {
"_eq": "tabs"
}
}
]
}
},
{
"hidden": true,
"name": "Not Tabs Template",
"options": {
"start": "closed"
},
"readonly": true,
"rule": {
"_and": [
{
"template": {
"_neq": "tabs"
}
}
]
}
}
],
"display": null,
"display_options": null,
"field": "Tabs",
"group": "accordion-ykcgp6",
"group": "Profile",
"hidden": false,
"interface": "group-detail",
"interface": "group-raw",
"note": null,
"options": {
"headerColor": "#1A5FB4"
"headerColor": "#1A5FB4",
"start": "closed"
},
"readonly": false,
"required": false,

View File

@ -10,7 +10,7 @@
"field": "active_tabs",
"group": "Tabs",
"hidden": false,
"interface": "group-detail",
"interface": "group-raw",
"note": null,
"options": null,
"readonly": false,

View File

@ -1,14 +1,30 @@
{
"collection": "oceannomads_profiles",
"field": "first_name",
"collection": "types",
"field": "cta_button_label",
"type": "string",
"meta": {
"collection": "oceannomads_profiles",
"conditions": null,
"collection": "types",
"conditions": [
{
"hidden": true,
"name": "show cta button",
"readonly": false,
"required": false,
"rule": {
"_and": [
{
"show_cta_button": {
"_eq": false
}
}
]
}
}
],
"display": null,
"display_options": null,
"field": "first_name",
"group": null,
"field": "cta_button_label",
"group": "header_elements",
"hidden": false,
"interface": "input",
"note": null,
@ -23,8 +39,8 @@
"width": "half"
},
"schema": {
"name": "first_name",
"table": "oceannomads_profiles",
"name": "cta_button_label",
"table": "types",
"data_type": "character varying",
"default_value": null,
"max_length": 255,

View File

@ -1,21 +1,21 @@
{
"collection": "types",
"field": "accordion-ykcgp6",
"field": "header_elements",
"type": "alias",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "accordion-ykcgp6",
"group": "Profile",
"field": "header_elements",
"group": "Header",
"hidden": false,
"interface": "group-accordion",
"interface": "group-raw",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 2,
"sort": 3,
"special": [
"alias",
"no-data",

View File

@ -8,14 +8,14 @@
"display": null,
"display_options": null,
"field": "profileTemplate",
"group": null,
"group": "Flex",
"hidden": false,
"interface": "list-m2a",
"note": null,
"options": {},
"readonly": false,
"required": false,
"sort": 11,
"sort": 1,
"special": [
"m2a"
],

View File

@ -1,31 +1,31 @@
{
"collection": "types",
"field": "onepager",
"field": "show_cta_button",
"type": "boolean",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "onepager",
"group": "accordion-ykcgp6",
"field": "show_cta_button",
"group": "header_elements",
"hidden": false,
"interface": "boolean",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 2,
"sort": 4,
"special": [
"cast-boolean"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
"width": "half"
},
"schema": {
"name": "onepager",
"name": "show_cta_button",
"table": "types",
"data_type": "boolean",
"default_value": false,

View File

@ -0,0 +1,45 @@
{
"collection": "types",
"field": "show_navigation_button",
"type": "boolean",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "show_navigation_button",
"group": "header_elements",
"hidden": false,
"interface": "boolean",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 2,
"special": [
"cast-boolean"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "show_navigation_button",
"table": "types",
"data_type": "boolean",
"default_value": false,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -1,14 +1,14 @@
{
"collection": "types",
"field": "text_area",
"field": "show_qr_button",
"type": "boolean",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "text_area",
"group": "accordion-ykcgp6",
"field": "show_qr_button",
"group": "header_elements",
"hidden": false,
"interface": "boolean",
"note": null,
@ -22,10 +22,10 @@
"translations": null,
"validation": null,
"validation_message": null,
"width": "full"
"width": "half"
},
"schema": {
"name": "text_area",
"name": "show_qr_button",
"table": "types",
"data_type": "boolean",
"default_value": false,

View File

@ -0,0 +1,45 @@
{
"collection": "types",
"field": "show_share_button",
"type": "boolean",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "show_share_button",
"group": "header_elements",
"hidden": false,
"interface": "boolean",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 3,
"special": [
"cast-boolean"
],
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "show_share_button",
"table": "types",
"data_type": "boolean",
"default_value": false,
"max_length": null,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -12,10 +12,13 @@
"hidden": false,
"interface": "group-detail",
"note": null,
"options": null,
"options": {
"headerIcon": "edit_square",
"start": "closed"
},
"readonly": false,
"required": false,
"sort": 8,
"sort": 9,
"special": [
"alias",
"no-data",

View File

@ -12,10 +12,13 @@
"hidden": false,
"interface": "group-detail",
"note": null,
"options": null,
"options": {
"headerIcon": "wysiwyg",
"start": "closed"
},
"readonly": false,
"required": false,
"sort": 7,
"sort": 8,
"special": [
"alias",
"no-data",

View File

@ -0,0 +1,74 @@
{
"collection": "types",
"field": "subtitle_label",
"type": "string",
"meta": {
"collection": "types",
"conditions": [
{
"hidden": false,
"name": "subtitle=custom",
"readonly": false,
"required": true,
"rule": {
"_and": [
{
"subtitle_mode": {
"_eq": "custom"
}
}
]
}
},
{
"hidden": true,
"name": "subtitle != custom",
"readonly": true,
"required": false,
"rule": {
"_and": [
{
"subtitle_mode": {
"_neq": "custom"
}
}
]
}
}
],
"display": null,
"display_options": null,
"field": "subtitle_label",
"group": "Header",
"hidden": false,
"interface": "input",
"note": null,
"options": null,
"readonly": false,
"required": false,
"sort": 2,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "subtitle_label",
"table": "types",
"data_type": "character varying",
"default_value": "Subname",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -0,0 +1,58 @@
{
"collection": "types",
"field": "subtitle_mode",
"type": "string",
"meta": {
"collection": "types",
"conditions": null,
"display": null,
"display_options": null,
"field": "subtitle_mode",
"group": "Header",
"hidden": false,
"interface": "select-dropdown",
"note": null,
"options": {
"choices": [
{
"text": "address",
"value": "address"
},
{
"text": "custom",
"value": "custom"
},
{
"text": "none",
"value": "none"
}
]
},
"readonly": false,
"required": false,
"sort": 1,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "subtitle_mode",
"table": "types",
"data_type": "character varying",
"default_value": "address",
"max_length": 255,
"numeric_precision": null,
"numeric_scale": null,
"is_nullable": true,
"is_unique": false,
"is_indexed": false,
"is_primary_key": false,
"is_generated": false,
"generation_expression": null,
"has_auto_increment": false,
"foreign_key_table": null,
"foreign_key_column": null
}
}

View File

@ -8,7 +8,7 @@
"display": null,
"display_options": null,
"field": "template",
"group": null,
"group": "Profile",
"hidden": false,
"interface": "select-dropdown",
"note": null,
@ -34,7 +34,7 @@
},
"readonly": false,
"required": false,
"sort": 10,
"sort": 1,
"special": null,
"translations": null,
"validation": null,

View File

@ -1,5 +1,5 @@
{
"version": 1,
"directus": "11.7.2",
"directus": "11.9.3",
"vendor": "postgres"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -64,3 +64,30 @@ ON CONFLICT (id) DO UPDATE
item = excluded.item,
sort = excluded.sort,
types_id = excluded.types_id;
-- Type: user:text+gallery
INSERT INTO public."types_profileTemplate" (collection, id, item, sort, types_id)
SELECT
'texts', '6', 'c960bbfc-5d98-4f6d-ae44-7a2b63d3359b' , '1', types.id
FROM
public.types as types
WHERE
name = 'user:text+gallery'
ON CONFLICT (id) DO UPDATE
SET collection = excluded.collection,
item = excluded.item,
sort = excluded.sort,
types_id = excluded.types_id;
INSERT INTO public."types_profileTemplate" (collection, id, item, sort, types_id)
SELECT
'gallery', '7', '6d18b616-6f4f-4987-9860-681b88bdc068' , '2', types.id
FROM
public.types as types
WHERE
name = 'user:text+gallery'
ON CONFLICT (id) DO UPDATE
SET collection = excluded.collection,
item = excluded.item,
sort = excluded.sort,
types_id = excluded.types_id;

View File

@ -1,6 +1,9 @@
{
"name": "directus-extensions",
"engines": {
"node": ">=22.20.0"
},
"dependencies": {
"directus-extension-sync": "^3.0.4"
"directus-extension-sync": "3.0.4"
}
}

24
backend/pull.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/sh
# base setup
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
DIRECTUS_URL="${DIRECTUS_URL:-http://localhost:8055}"
DIRECTUS_EMAIL="${DIRECTUS_EMAIL:-admin@it4c.dev}"
DIRECTUS_PASSWORD="${DIRECTUS_PASSWORD:-admin123}"
PGPASSWORD="${PGPASSWORD:-'directus'}"
PGUSER="${PGUSER:-'directus'}"
PGDATABASE="${PGDATABASE:-'directus'}"
PROJECT_NAME="${PROJECT:-development}"
PROJECT_FOLDER=$SCRIPT_DIR/directus-config/$PROJECT_NAME
echo "Pull collections"
npx directus-sync@3.4.0 pull \
--dump-path $PROJECT_FOLDER \
--directus-url $DIRECTUS_URL \
--directus-email $DIRECTUS_EMAIL \
--directus-password $DIRECTUS_PASSWORD \
|| exit 1

24
backend/push.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/sh
# base setup
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
DIRECTUS_URL="${DIRECTUS_URL:-http://localhost:8055}"
DIRECTUS_EMAIL="${DIRECTUS_EMAIL:-admin@it4c.dev}"
DIRECTUS_PASSWORD="${DIRECTUS_PASSWORD:-admin123}"
PGPASSWORD="${PGPASSWORD:-'directus'}"
PGUSER="${PGUSER:-'directus'}"
PGDATABASE="${PGDATABASE:-'directus'}"
PROJECT_NAME="${PROJECT:-development}"
PROJECT_FOLDER=$SCRIPT_DIR/directus-config/$PROJECT_NAME
echo "Push collections"
npx directus-sync@3.4.0 push \
--dump-path $PROJECT_FOLDER \
--directus-url $DIRECTUS_URL \
--directus-email $DIRECTUS_EMAIL \
--directus-password $DIRECTUS_PASSWORD \
|| exit 1

View File

@ -15,16 +15,8 @@ PGDATABASE="${PGDATABASE:-'directus'}"
PROJECT_NAME="${PROJECT:-development}"
PROJECT_FOLDER=$SCRIPT_DIR/directus-config/$PROJECT_NAME
echo "Sync collections"
npx directus-sync push \
--dump-path $PROJECT_FOLDER \
--directus-url $DIRECTUS_URL \
--directus-email $DIRECTUS_EMAIL \
--directus-password $DIRECTUS_PASSWORD \
|| exit 1
echo "Seed data"
npx directus-sync seed push \
npx directus-sync@3.4.0 seed push \
--seed-path $PROJECT_FOLDER/seed \
--directus-url $DIRECTUS_URL \
--directus-email $DIRECTUS_EMAIL \

169
cypress/README.md Normal file
View File

@ -0,0 +1,169 @@
# Cypress End-to-End Tests
This directory contains **end-to-end tests** for the **Utopia Map application** using [Cypress](https://www.cypress.io/).
## Technology Stack
- **Cypress** - E2E testing framework with TypeScript
- **cypress-split** - Parallel test execution for faster runs - in the CI pipeline and locally
- **mochawesome** - HTML report generation with embedded screenshots for the Github CI pipeline
## GitHub CI Integration
Tests run automatically on every push via the `.github/workflows/test.e2e.yml` workflow:
1. **Build** - Compiles the library and frontend application
2. **Start Services** - Launches Docker stack (frontend, backend, database)
3. **Seed Data** - Populates database with test data
4. **Run Tests** - Executes tests in parallel using `cypress-split`
5. **Generate Reports** - Creates consolidated HTML report with screenshots
6. **Upload Artifacts** - On failure, uploads test reports for debugging
### Parallel Execution
Tests run in parallel using [cypress-split](https://github.com/bahmutov/cypress-split), automatically distributing spec files across multiple processes to reduce execution time.
### Test Reports
When tests fail, GitHub Actions automatically uploads as artefact:
- **HTML Report**
- Interactive test results with embedded screenshots
- Available for 14 days with unique naming: `e2e-test-report-{run-id}`
## Running Tests Locally
### Prerequisites
- Node.js (version from `.tool-versions`)
- Docker and Docker Compose
- Sufficient disk space (~2GB for Docker images)
### Headless Mode (CI-like)
Run tests without GUI, replicating the CI environment:
```bash
# 1. Set Node.js version
nvm use
# 2. Build the library and frontend
cd lib && npm install && npm run build && cd ..
# 3.Build the frontend && cd ..
cd app && cp .env.dist .env
sed -i '/VITE_DIRECTUS_ADMIN_ROLE=/c\VITE_DIRECTUS_ADMIN_ROLE=8141dee8-8e10-48d0-baf1-680aea271298' .env
npm ci && npm run build && cd ..
# 3. Start Docker services
docker compose up -d
# 4. Wait for services and seed data
timeout 120 bash -c 'until curl -f http://localhost:8055/server/health; do sleep 5; done'
cd backend && ./seed.sh && cd ..
# 5. Run tests
cd cypress && npm ci
# Run all tests in parallel (like CI)
npm run test:split:auto
# Or run tests sequentially
npm test
# Or run specific test file
npx cypress run --e2e --browser chromium --spec "e2e/authentication/login.cy.ts"
```
### GUI Mode (Development)
Run tests with interactive GUI for debugging:
```bash
# 1-4. Follow steps 1-4 from headless mode above
# 5. Open Cypress GUI
cd cypress && npm ci && npm run test:open
```
#### GUI Features
- **Live Reload** - Tests auto-reload when you save changes
- **Time Travel** - Click commands to see DOM snapshots
- **Selector Playground** - Interactive tool to find element selectors
- **Network Inspection** - View all XHR/fetch requests
- **Debug Tools** - Use browser DevTools for debugging
### Cleanup
```bash
# Stop containers
docker compose down
# Remove database data (for fresh start)
sudo rm -rf ./data/database
```
## Test Reports
After running tests, reports are generated in `cypress/results/`:
- **HTML Report** - `results/html/merged-report.html` - Interactive report with charts
- **Screenshots** - `results/html/screenshots/` - Failure screenshots embedded in HTML
- **JSON Data** - `results/*.json` - Raw test data for custom processing
Generate reports manually:
```bash
npm run report:merge # Merge parallel test results
npm run report:generate # Create HTML report
```
## Writing Tests
Tests are located in `cypress/e2e/` and follow this structure:
```typescript
describe('Feature Name', () => {
beforeEach(() => {
cy.clearCookies()
cy.clearLocalStorage()
cy.visit('/page-url')
})
it('should perform expected behavior', () => {
cy.get('[data-cy="input"]').type('value')
cy.get('[data-cy="submit"]').click()
cy.url().should('include', '/success')
})
})
```
### Best Practices
- Use `[data-cy="..."]` selectors for stability
- Test user behavior, rather than just implementation details
- Keep tests isolated and independent
- Use custom commands for common patterns
- Clear state in `beforeEach` hooks
## Troubleshooting
**Tests fail with "baseUrl not reachable"**
```bash
cd app && npm run build && cd .. && docker compose up -d
```
**Backend health check fails**
```bash
docker compose logs backend
docker compose down && docker compose up -d
```
**Seeding fails with "ConflictError"**
```bash
docker compose down && sudo rm -rf ./data/database && docker compose up -d
```
**Permission denied on ./data**
```bash
sudo chmod 777 -R ./data
```

67
cypress/cypress.config.ts Normal file
View File

@ -0,0 +1,67 @@
import { defineConfig } from 'cypress'
const cypressSplit = require('cypress-split')
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:8080',
viewportWidth: 1280,
viewportHeight: 720,
specPattern: 'e2e/**/*.cy.ts',
supportFile: 'support/e2e.ts',
screenshotsFolder: 'screenshots',
videosFolder: 'videos',
video: false,
screenshotOnRunFailure: true,
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'results',
reportFilename: '[name]',
overwrite: false,
html: false, // Only generate JSON during test runs for merging
json: true, // Generate JSON for merging
embeddedScreenshots: true,
useInlineDiffs: true,
screenshotOnRunFailure: true
},
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
pageLoadTimeout: 30000,
testIsolation: true,
retries: {
runMode: 2,
openMode: 0
},
env: {
apiUrl: 'http://localhost:8055',
validEmail: 'admin@it4c.dev',
validPassword: 'admin123',
invalidEmail: 'invalid@example.com',
invalidPassword: 'wrongpassword'
},
setupNodeEvents(on, config) {
// Load cypress-split plugin
cypressSplit(on, config)
// Load parallel reporter plugin
const parallelReporter = require('./plugins/parallel-reporter')
config = parallelReporter(on, config)
on('task', {
log(message) {
console.log(message)
return null
}
})
return config
},
},
})

View File

@ -0,0 +1,166 @@
/// <reference types="cypress" />
describe('Utopia Map Login', () => {
const testData = {
validUser: {
email: Cypress.env('validEmail'),
password: Cypress.env('validPassword')
},
invalidUser: {
email: Cypress.env('invalidEmail'),
password: Cypress.env('invalidPassword')
}
}
beforeEach(() => {
cy.clearCookies()
cy.clearLocalStorage()
cy.window().then((win) => {
win.sessionStorage.clear()
})
cy.visit('/login')
})
it('should successfully login with valid credentials', () => {
cy.intercept('POST', '**/auth/login').as('loginRequest')
cy.get('input[type="email"]').clear()
cy.get('input[type="email"]').type(testData.validUser.email)
cy.get('input[type="password"]').clear()
cy.get('input[type="password"]').type(testData.validUser.password)
cy.get('button:contains("Login")').click()
cy.wait('@loginRequest').then((interception) => {
expect(interception.response?.statusCode).to.eq(200)
expect(interception.request.body).to.deep.include({
email: testData.validUser.email,
password: testData.validUser.password
})
expect(interception.response?.body).to.have.property('data')
expect(interception.response?.body.data).to.have.property('access_token')
expect(interception.response?.body.data.access_token).to.be.a('string')
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(interception.response?.body.data.access_token).to.not.be.empty
})
cy.get('.Toastify__toast--success', { timeout: 10000 }).should('be.visible')
cy.url().should('eq', Cypress.config().baseUrl + '/')
})
it('should show error for missing password', () => {
cy.intercept('POST', '**/auth/login').as('loginRequest')
cy.get('input[type="email"]').type(testData.validUser.email)
cy.get('button:contains("Login")').click()
cy.wait('@loginRequest').then((interception) => {
expect(interception.response?.statusCode).to.eq(400)
expect(interception.request.body).to.deep.include({
email: testData.validUser.email,
password: ''
})
expect(interception.response?.body).to.have.property('errors')
expect(interception.response?.body.errors).to.be.an('array')
expect(interception.response?.body.errors).to.have.length.greaterThan(0)
expect(interception.response?.body.errors[0]).to.have.property('message')
})
cy.get('.Toastify__toast--error', { timeout: 10000 }).should('be.visible')
cy.url().should('include', '/login')
})
it('should show error for missing email', () => {
cy.intercept('POST', '**/auth/login').as('loginRequest')
cy.get('input[type="password"]').type(testData.validUser.password)
cy.get('button:contains("Login")').click()
cy.wait('@loginRequest').then((interception) => {
expect(interception.response?.statusCode).to.eq(400)
expect(interception.request.body).to.deep.include({
email: '',
password: testData.validUser.password
})
expect(interception.response?.body).to.have.property('errors')
expect(interception.response?.body.errors).to.be.an('array')
expect(interception.response?.body.errors).to.have.length.greaterThan(0)
})
cy.get('.Toastify__toast--error', { timeout: 10000 }).should('be.visible')
cy.url().should('include', '/login')
})
it('should show error for missing credentials', () => {
cy.intercept('POST', '**/auth/login').as('loginRequest')
cy.get('button:contains("Login")').click()
cy.wait('@loginRequest').then((interception) => {
expect(interception.response?.statusCode).to.eq(400)
expect(interception.request.body).to.deep.include({
email: '',
password: ''
})
expect(interception.response?.body).to.have.property('errors')
expect(interception.response?.body.errors).to.be.an('array')
expect(interception.response?.body.errors).to.have.length.greaterThan(0)
})
cy.get('.Toastify__toast--error', { timeout: 10000 }).should('be.visible')
cy.url().should('include', '/login')
})
it('should show error for invalid credentials', () => {
cy.intercept('POST', '**/auth/login').as('loginRequest')
cy.get('input[type="email"]').clear()
cy.get('input[type="email"]').type(testData.invalidUser.email)
cy.get('input[type="password"]').clear()
cy.get('input[type="password"]').type(testData.invalidUser.password)
cy.get('button:contains("Login")').click()
cy.wait('@loginRequest').then((interception) => {
expect(interception.response?.statusCode).to.eq(401)
expect(interception.request.body).to.deep.include({
email: testData.invalidUser.email,
password: testData.invalidUser.password
})
expect(interception.response?.body).to.have.property('errors')
expect(interception.response?.body.errors).to.be.an('array')
expect(interception.response?.body.errors[0]).to.have.property('message')
expect(interception.response?.body.errors[0].message).to.contain('Invalid user credentials')
})
cy.get('.Toastify__toast--error', { timeout: 10000 }).should('be.visible')
cy.url().should('include', '/login')
})
it('should show loading state during login', () => {
cy.intercept('POST', '**/auth/login', {
delay: 1000,
statusCode: 200,
body: {
data: {
access_token: 'test_token_123',
expires: 900000,
refresh_token: 'refresh_token_123'
}
}
}).as('loginRequest')
cy.get('input[type="email"]').type(testData.validUser.email)
cy.get('input[type="password"]').type(testData.validUser.password)
cy.get('button:contains("Login")').click()
cy.get('.tw\\:loading-spinner', { timeout: 5000 }).should('be.visible')
cy.wait('@loginRequest')
cy.url().should('eq', Cypress.config().baseUrl + '/')
})
})

View File

@ -0,0 +1,33 @@
/// <reference types="cypress" />
describe('Utopia Map Login Form Elements', () => {
beforeEach(() => {
cy.clearCookies()
cy.clearLocalStorage()
cy.window().then((win) => {
win.sessionStorage.clear()
})
cy.visit('/login')
})
it('should be displayed correctly', () => {
cy.get('h2').should('contain.text', 'Login')
cy.get('input[type="email"]')
.should('be.visible')
.should('have.attr', 'placeholder', 'E-Mail')
cy.get('input[type="password"]')
.should('be.visible')
.should('have.attr', 'placeholder', 'Password')
cy.get('button:contains("Login")')
.should('be.visible')
.should('not.be.disabled')
cy.get('a[href="/reset-password"]')
.should('be.visible')
.should('contain.text', 'Forgot Password?')
})
})

View File

@ -0,0 +1,115 @@
/// <reference types="cypress" />
describe('Utopia Map Search', () => {
beforeEach(() => {
cy.clearCookies()
cy.clearLocalStorage()
cy.window().then((win) => {
win.sessionStorage.clear()
})
cy.visit('/')
cy.waitForMapReady()
})
describe('Item Search', () => {
it('should find items by exact name match', () => {
cy.searchFor('Tech Meetup Munich')
cy.get('[data-cy="search-suggestions"]').should('contain', 'Tech Meetup Munich')
})
it('should find items by partial name match (case insensitive)', () => {
cy.searchFor('café collaboration')
cy.get('[data-cy="search-suggestions"]').should('contain', 'Café Collaboration London')
})
it('should find items by text content', () => {
cy.searchFor('sustainability')
cy.get('[data-cy="search-suggestions"]').should('contain', 'Alex Entrepreneur')
})
it('should navigate to item profile when clicking search result', () => {
cy.searchFor('welcome')
cy.get('[data-cy="search-suggestions"]').within(() => {
cy.get('[data-cy="search-item-result"]').contains('Welcome to Utopia Map').click()
})
cy.url().should('match', /\/(item\/|[a-f0-9-]+)/)
cy.get('body').should('contain', 'Welcome to Utopia Map')
})
})
describe('Geographic Search', () => {
it('should find geographic locations and related items for a city', () => {
cy.searchFor('Berlin')
cy.get('[data-cy="search-suggestions"]').within(() => {
cy.get('[data-cy="search-geo-result"]').should('contain', 'Berlin')
cy.get('[data-cy="search-item-result"]').should('contain', 'Community Garden Berlin')
})
})
it('should navigate to geographic location when clicking search result', () => {
cy.searchFor('Berlin')
// Click geographic result -> temporary marker
cy.get('[data-cy="search-suggestions"]').within(() => {
cy.get('[data-cy="search-geo-result"]').contains('Berlin').click()
})
// User sees temporary marker with location popup
cy.get('.leaflet-popup').should('be.visible')
cy.get('.leaflet-popup-content').should('contain', 'Berlin')
// Search input is blurred and suggestions hidden
cy.get('[data-cy="search-input"]').should('not.be.focused')
cy.get('[data-cy="search-suggestions"]').should('not.exist')
})
it('should find specific addresses and landmarks', () => {
cy.searchFor('Wat Arun')
cy.get('[data-cy="search-suggestions"]').should('be.visible')
cy.get('[data-cy="search-suggestions"]').within(() => {
cy.contains('Wat Arun').first().click()
})
cy.get('.leaflet-popup').should('be.visible')
cy.get('.leaflet-popup-content').should('contain', 'Wat Arun')
})
it('should navigate to precise coordinates', () => {
const coordinates = '52.5200,13.4050'
cy.searchFor(coordinates)
// User sees coordinate option with flag icon
cy.get('[data-cy="search-suggestions"]').within(() => {
cy.get('[data-cy="search-coordinate-result"]').should('contain', coordinates)
cy.get('[data-cy="search-coordinate-icon"]').should('exist')
cy.get('[data-cy="search-coordinate-result"]').click()
})
cy.get('.leaflet-popup').should('be.visible')
cy.get('.leaflet-popup-content').should('contain.text', '52.52, 13.40')
})
it('should differentiate between database items and geographic locations', () => {
cy.searchFor('Berlin')
cy.get('[data-cy="search-suggestions"]').within(() => {
// Database item should have custom icon and simple name
cy.get('[data-cy="search-item-result"]').first().within(() => {
// Should have either an icon or placeholder for database items
cy.get('[data-cy="search-item-icon"], [data-cy="search-item-icon-placeholder"]').should('exist')
})
cy.get('[data-cy="search-item-result"]').should('contain', 'Community Garden Berlin')
// Geographic result should have magnifying glass icon and detailed info
cy.get('[data-cy="search-geo-result"]').first().within(() => {
cy.get('[data-cy="search-geo-icon"]').should('exist')
cy.get('[data-cy="search-geo-details"]').should('exist')
})
})
})
})
})

133
cypress/eslint.config.mjs Normal file
View File

@ -0,0 +1,133 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginCypress from 'eslint-plugin-cypress'
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.cy.{js,jsx,ts,tsx}', 'cypress/**/*.{js,jsx,ts,tsx}'],
plugins: {
cypress: pluginCypress,
},
languageOptions: {
globals: {
cy: 'readonly',
Cypress: 'readonly',
expect: 'readonly',
assert: 'readonly',
chai: 'readonly',
describe: 'readonly',
it: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
before: 'readonly',
after: 'readonly',
},
},
rules: {
'cypress/no-assigning-return-values': 'error',
'cypress/no-unnecessary-waiting': 'error',
'cypress/no-async-tests': 'error',
'cypress/unsafe-to-chain-command': 'error',
'@typescript-eslint/no-unused-vars': ['error', {
'argsIgnorePattern': '^_',
'varsIgnorePattern': '^_'
}],
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': 'warn',
'no-debugger': 'error',
'no-unused-vars': 'warn',
'prefer-const': 'error',
'no-var': 'error',
'eqeqeq': ['error', 'always'],
'curly': ['error', 'all'],
'indent': ['error', 2],
'quotes': ['error', 'single', { 'avoidEscape': true }],
'semi': ['error', 'never'],
'comma-dangle': ['error', 'never'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
'space-before-function-paren': ['error', 'never'],
'keyword-spacing': ['error', { 'before': true, 'after': true }],
'space-infix-ops': 'error',
'eol-last': ['error', 'always'],
'no-trailing-spaces': 'error',
'max-len': ['warn', {
'code': 100,
'ignoreUrls': true,
'ignoreStrings': true,
'ignoreTemplateLiterals': true
}]
}
},
{
files: ['cypress/support/**/*.{js,ts}'],
rules: {
// Enable console warnings in support files
'no-console': 'warn'
}
},
{
files: ['cypress.config.{js,ts}', 'eslint.config.js'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-require-imports': 'off'
}
},
// Node.js CommonJS files (plugins, etc.) - exclude TypeScript rules
{
files: ['plugins/**/*.js'],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'commonjs',
globals: {
require: 'readonly',
module: 'readonly',
exports: 'readonly',
process: 'readonly',
console: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
Buffer: 'readonly',
global: 'readonly'
}
},
rules: {
// Disable TypeScript-specific rules for CommonJS files
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
// Allow CommonJS patterns
'no-undef': 'off',
'no-console': 'off',
// Keep basic JS rules
'no-unused-vars': 'warn',
'prefer-const': 'error',
'no-var': 'error'
}
},
{
ignores: [
'node_modules/**',
'cypress/downloads/**',
'cypress/screenshots/**',
'cypress/videos/**',
'cypress/plugins/**', // Ignore Node.js CommonJS plugin files
'results/**',
'dist/**',
'build/**',
'*.min.js'
]
}
)

6457
cypress/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
cypress/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "utopia-map-e2e-testing",
"private": true,
"version": "1.0.0",
"description": "Cypress End-to-End Tests for Utopia Map",
"scripts": {
"test": "cypress run --e2e --browser chromium",
"test:open": "cypress open --e2e",
"report:merge": "mochawesome-merge 'results/*.json' -o results/merged-report.json",
"report:generate": "marge results/merged-report.json --reportDir results/html --reportTitle 'Utopia Map E2E Tests' --reportPageTitle 'Utopia Map E2E Test Report' --inline --charts --showPassed --showFailed --showPending --showSkipped",
"report:full": "npm run report:merge && npm run report:generate",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"keywords": [
"cypress",
"cypress-split",
"e2e",
"mochawesome",
"testing",
"utopia-map"
],
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/mochawesome": "^6.2.4",
"@types/node": "^24.5.2",
"cypress": "^15.3.0",
"cypress-split": "^1.24.23",
"eslint": "^9.36.0",
"eslint-plugin-cypress": "^5.1.1",
"mochawesome": "^7.1.4",
"mochawesome-merge": "^4.3.0",
"mochawesome-report-generator": "^6.2.0",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1"
}
}

View File

@ -0,0 +1,71 @@
/**
* Cypress Plugin for Enhanced Parallel Test Reporting
* Handles mochawesome report generation for parallel test execution
*/
const path = require('path')
const fs = require('fs')
module.exports = (on, config) => {
// Ensure results directory exists
const resultsDir = path.join(config.projectRoot, 'results')
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true })
}
// Configure mochawesome for parallel execution - JSON only for merging
const splitIndex = process.env.SPLIT_INDEX || '0'
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
config.reporterOptions = {
...config.reporterOptions,
reportFilename: `split-${splitIndex}-${timestamp}-[name]`,
reportDir: resultsDir,
overwrite: false,
html: false, // No individual HTML files
json: true, // Only JSON for merging into one report
embeddedScreenshots: true,
useInlineDiffs: true
}
// Task for logging parallel execution info
on('task', {
log(message) {
console.log(`[Parallel ${splitIndex}] ${message}`)
return null
},
logReportInfo() {
console.log(`[Parallel ${splitIndex}] Report will be saved as: report-${splitIndex}-${timestamp}.json`)
return null
}
})
// Before run hook
on('before:run', (details) => {
console.log(`[Parallel ${splitIndex}] Starting test execution`)
console.log(`[Parallel ${splitIndex}] Browser: ${details.browser.name}`)
console.log(`[Parallel ${splitIndex}] Specs: ${details.specs.length}`)
return details
})
// After run hook
on('after:run', (results) => {
console.log(`[Parallel ${splitIndex}] Test execution completed`)
console.log(`[Parallel ${splitIndex}] Total tests: ${results.totalTests}`)
console.log(`[Parallel ${splitIndex}] Passed: ${results.totalPassed}`)
console.log(`[Parallel ${splitIndex}] Failed: ${results.totalFailed}`)
// Ensure the report file was created
const reportFile = path.join(resultsDir, `report-${splitIndex}-${timestamp}.json`)
if (fs.existsSync(reportFile)) {
console.log(`[Parallel ${splitIndex}] ✅ Report saved: ${reportFile}`)
} else {
console.log(`[Parallel ${splitIndex}] ❌ Report not found: ${reportFile}`)
}
return results
})
return config
}

View File

@ -0,0 +1,71 @@
#!/bin/bash
set -e
echo "=== Creating index page for consolidated report ==="
cat > results/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>Utopia Map E2E Test Report</title>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f8f9fa; }
.header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px #00000020; }
.report-section { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px #00000020; }
.report-link { display: block; padding: 16px 20px; margin: 12px 0; background: #e3f2fd; border-radius: 8px; text-decoration: none; color: #1976d2; border-left: 4px solid #2196f3; font-size: 18px; font-weight: 500; }
.report-link:hover { background: #bbdefb; }
.meta { color: #666; font-size: 14px; margin: 4px 0; }
.status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; }
.status.failed { background: #ffebee; color: #c62828; }
.status.passed { background: #e8f5e8; color: #2e7d32; }
</style>
</head>
<body>
<div class="header">
<h1>Utopia Map E2E Test Report</h1>
<div class="meta">Generated: $(date)</div>
<div class="meta">Run ID: ${GITHUB_RUN_ID:-unknown}</div>
<div class="meta">Commit: ${GITHUB_SHA:-unknown}</div>
<div class="meta">Status: <span class="status failed">Tests Failed</span></div>
</div>
<div class="report-section">
<h2>📊 Consolidated Test Report</h2>
<p>This report contains all test results from the parallel test execution, merged into one comprehensive view with screenshots embedded directly in failing test cases.</p>
<a href="merged-report.html" class="report-link">
📈 View Complete Test Report (All Specs)
</a>
</div>
<div class="report-section">
<h2>📸 Test Screenshots</h2>
<p>Screenshots are automatically embedded within their respective failing test cases in the main report. No separate screenshot viewing required.</p>
</div>
<script>
setTimeout(() => {
window.location.href = 'merged-report.html';
}, 3000);
let countdown = 3;
const countdownEl = document.createElement('div');
countdownEl.style.cssText = 'position: fixed; top: 20px; right: 20px; background: #2196f3; color: white; padding: 10px 15px; border-radius: 4px; font-size: 14px;';
countdownEl.textContent = `Auto-redirecting in ${countdown}s...`;
document.body.appendChild(countdownEl);
const timer = setInterval(() => {
countdown--;
if (countdown > 0) {
countdownEl.textContent = `Auto-redirecting in ${countdown}s...`;
} else {
clearInterval(timer);
countdownEl.textContent = 'Redirecting...';
}
}, 1000);
</script>
</body>
</html>
EOF
echo "✅ Simple index page created with auto-redirect to consolidated report"

View File

@ -0,0 +1,96 @@
#!/bin/bash
set -e
echo "=== Generating HTML report with embedded screenshots ==="
if [ ! -f "results/merged-report.json" ]; then
echo "❌ No merged JSON found, cannot generate consolidated report"
exit 1
fi
echo "Generating comprehensive HTML report from merged JSON..."
npm run report:generate
if [ ! -f "results/html/merged-report.html" ]; then
echo "❌ HTML generation failed"
exit 1
fi
echo "✅ Consolidated HTML report generated successfully"
echo "Report size: $(wc -c < results/html/merged-report.html) bytes"
# Copy screenshots with proper structure for the HTML report
echo "Copying screenshots to HTML report directory..."
if [ -d "screenshots" ]; then
echo "Screenshots directory found"
# Remove unwanted screenshots (like before-intentional-failure)
echo "Cleaning up unwanted screenshots..."
find screenshots -name "*before-intentional-failure*" -type f -delete 2>/dev/null || true
find screenshots -name "*before-*" -type f -delete 2>/dev/null || true
# Create screenshots directory in the HTML output
mkdir -p "results/html/screenshots"
# Extract all screenshot paths expected by the HTML report and copy them accordingly
echo "Extracting screenshot paths from HTML report..."
# Create screenshots directory in the HTML output
mkdir -p "results/html/screenshots"
if [ -f "results/merged-report.json" ]; then
echo "Reading expected screenshot paths from JSON report..."
# Extract all screenshot paths referenced in the JSON report (from context fields)
grep -o 'screenshots/[^"]*\.png' results/merged-report.json | sort -u | while read expected_path; do
# Extract components from expected path: screenshots/parent-dir/test-file/filename.png
if [[ "$expected_path" =~ screenshots/([^/]+)/([^/]+)/(.+) ]]; then
parent_dir="${BASH_REMATCH[1]}"
test_file="${BASH_REMATCH[2]}"
filename="${BASH_REMATCH[3]}"
# Try to find the actual screenshot in various possible locations
actual_screenshot=""
# 1. Try full structure first: screenshots/parent-dir/test-file/filename.png
if [ -f "screenshots/$parent_dir/$test_file/$filename" ]; then
actual_screenshot="screenshots/$parent_dir/$test_file/$filename"
# 2. Try flat structure: screenshots/test-file/filename.png
elif [ -f "screenshots/$test_file/$filename" ]; then
actual_screenshot="screenshots/$test_file/$filename"
# 3. Try direct file: screenshots/filename.png
elif [ -f "screenshots/$filename" ]; then
actual_screenshot="screenshots/$filename"
fi
if [ -n "$actual_screenshot" ] && [ -f "$actual_screenshot" ]; then
# Create the expected directory structure in results/html
target_path="results/html/$expected_path"
target_dir=$(dirname "$target_path")
mkdir -p "$target_dir"
# Copy the screenshot to the expected location
cp "$actual_screenshot" "$target_path"
echo "Mapped screenshot: $(basename "$test_file") -> $parent_dir/$test_file"
fi
fi
done
else
echo "❌ No JSON report found, cannot determine expected screenshot paths"
# Fallback: copy whatever structure exists
if [ -d "screenshots" ] && [ "$(find screenshots -name '*.png' | wc -l)" -gt 0 ]; then
echo "Fallback: copying existing screenshot structure..."
cp -r screenshots/* results/html/screenshots/ 2>/dev/null || true
fi
fi
echo "✅ Screenshots copied successfully"
echo "Final screenshot structure:"
find results/html/screenshots -type f -name "*.png" | head -10
else
echo "⚠️ No screenshots directory found"
fi
echo "=== Final consolidated report ready ==="
ls -la results/html/

View File

@ -0,0 +1,35 @@
#!/bin/bash
set -e
echo "=== Merging all JSON reports into one consolidated report ==="
json_count=$(find results/ -name "*.json" -type f 2>/dev/null | wc -l)
echo "Found $json_count JSON report files from parallel test execution"
if [ "$json_count" -gt 0 ]; then
echo "=== JSON files found ==="
find results/ -name "*.json" -type f | sort
echo "=== Merging all reports into one ==="
npm run report:merge
if [ ! -f "results/merged-report.json" ]; then
echo "❌ Merge failed - no merged-report.json created"
exit 1
fi
echo "✅ Successfully merged $json_count JSON reports into one"
echo "Merged report size: $(wc -c < results/merged-report.json) bytes"
report=$(cat results/merged-report.json)
echo "Consolidated report stats:"
echo " - Total tests: $(echo "$report" | node -pe 'JSON.parse(require("fs").readFileSync(0)).stats?.tests || 0')"
echo " - Passed: $(echo "$report" | node -pe 'JSON.parse(require("fs").readFileSync(0)).stats?.passes || 0')"
echo " - Failed: $(echo "$report" | node -pe 'JSON.parse(require("fs").readFileSync(0)).stats?.failures || 0')"
echo " - Duration: $(echo "$report" | node -pe 'JSON.parse(require("fs").readFileSync(0)).stats?.duration || 0')ms"
else
echo "❌ No JSON reports found to merge"
echo "Creating empty report structure..."
mkdir -p results
echo '{"stats":{"tests":0,"passes":0,"failures":0},"results":[]}' > results/merged-report.json
fi

Some files were not shown because too many files have changed in this diff Show More