Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"dev": "vite --host",
"build": "tsc && vite build",
"test:lint:eslint": "eslint --max-warnings 0 .",
"preview": "vite preview"
"preview": "vite preview",
"test:unit": "vitest run"
},
"dependencies": {
"@directus/sdk": "^17.0.2",
Expand Down Expand Up @@ -50,6 +51,7 @@
"typescript": "^5.9.3",
"typescript-eslint": "^8.9.0",
"vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0"
"vite-plugin-pwa": "^1.2.0",
"vitest": "^4.0.16"
}
}
76 changes: 76 additions & 0 deletions app/src/api/layerLabels.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable camelcase -- `maps_id` is the Directus junction field name */
import { describe, expect, it } from 'vitest'

import { applyMapLabelOverrides } from './layerLabels'

const MAP = 'map-de'
const OTHER_MAP = 'map-en'

const layer = (maps: unknown) => ({ name: 'Gärten', menuText: 'Garten eintragen', maps })

describe('applyMapLabelOverrides', () => {
it('uses the override belonging to the requested map', () => {
const [result] = applyMapLabelOverrides(
[
layer([
{ maps_id: OTHER_MAP, name: 'Gardens', menuText: 'add a garden' },
{ maps_id: MAP, name: null, menuText: null },
]),
],
OTHER_MAP,
)

expect(result.name).toBe('Gardens')
expect(result.menuText).toBe('add a garden')
})

it('falls back to the layer label when the override is empty', () => {
const [result] = applyMapLabelOverrides(
[layer([{ maps_id: MAP, name: null, menuText: ' ' }])],
MAP,
)

expect(result.name).toBe('Gärten')
expect(result.menuText).toBe('Garten eintragen')
})

it('overrides each label independently', () => {
const [result] = applyMapLabelOverrides(
[layer([{ maps_id: MAP, name: 'Gemeinschaftsgärten', menuText: null }])],
MAP,
)

expect(result.name).toBe('Gemeinschaftsgärten')
expect(result.menuText).toBe('Garten eintragen')
})

it('accepts an expanded maps_id object', () => {
const [result] = applyMapLabelOverrides(
[layer([{ maps_id: { id: MAP }, name: 'Gemeinschaftsgärten' }])],
MAP,
)

expect(result.name).toBe('Gemeinschaftsgärten')
})

it('ignores junction rows of other maps and dangling rows', () => {
const [result] = applyMapLabelOverrides(
[
layer([
{ maps_id: null, name: 'Waise' },
{ maps_id: OTHER_MAP, name: 'Gardens' },
]),
],
MAP,
)

expect(result.name).toBe('Gärten')
})

it('leaves layers untouched when the junction is not expanded', () => {
const input = layer([701, 702])
const [result] = applyMapLabelOverrides([input], MAP)

expect(result).toBe(input)
})
})
46 changes: 46 additions & 0 deletions app/src/api/layerLabels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* A row of the `layers_maps` junction, which represents "this layer, on this map".
* `name` and `menuText` are optional per-map overrides: when a layer is shared
* between maps, each map can label it in its own language.
*/
interface LayerMapAssignment {
maps_id?: string | { id?: string } | null
name?: string | null
menuText?: string | null
}

const assignedMapId = (assignment: LayerMapAssignment): string | undefined =>
typeof assignment.maps_id === 'string'
? assignment.maps_id
: (assignment.maps_id?.id ?? undefined)

/** Treats null, undefined and blank strings alike, so an emptied field falls back. */
const override = (value: string | null | undefined, fallback: string): string =>
typeof value === 'string' && value.trim() !== '' ? value : fallback

const isAssignment = (candidate: unknown): candidate is LayerMapAssignment =>
typeof candidate === 'object' && candidate !== null

/**
* Applies the per-map label overrides stored on the `layers_maps` junction.
* Layers without an override for `mapId` are returned untouched, so maps that
* never set one keep the labels defined on the layer itself.
*/
export function applyMapLabelOverrides<
T extends { name?: string; menuText?: string; maps?: unknown },
>(layers: T[], mapId: string): T[] {
return layers.map((layer) => {
const assignments = Array.isArray(layer.maps) ? layer.maps : []
const assignment = assignments
.filter(isAssignment)
.find((candidate) => assignedMapId(candidate) === mapId)

if (!assignment) return layer

return {
...layer,
name: override(assignment.name, layer.name ?? ''),
menuText: override(assignment.menuText, layer.menuText ?? ''),
}
})
}
10 changes: 7 additions & 3 deletions app/src/api/layersApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import { readItems } from '@directus/sdk'

import { directusClient } from './directus'
import { applyMapLabelOverrides } from './layerLabels'

import type { LayerProps } from 'utopia-ui'

export class layersApi {
mapId: string
Expand All @@ -14,20 +17,21 @@ export class layersApi {

async getItems() {
try {
const layers = await directusClient.request(
const layers = (await directusClient.request(
readItems('layers' as any, {
fields: [
'*',
{ itemType: ['*.*', { profileTemplate: ['*', 'item.*.*.*.*'] }] },
{ markerIcon: ['*'] } as any,
{ maps: ['maps_id', 'name', 'menuText'] } as any,
],
// eslint-disable-next-line camelcase
filter: { maps: { maps_id: { id: { _eq: this.mapId } } } },
limit: 500,
sort: ['sort'],
}),
)
return layers
)) as unknown as LayerProps[]
return applyMapLabelOverrides(layers, this.mapId)
} catch (error: any) {
console.log(error)
if (error.errors[0]?.message) throw error.errors[0].message
Expand Down
5 changes: 5 additions & 0 deletions app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
Expand All @@ -20,6 +21,10 @@ export default defineConfig({
*/
},
plugins: [react(), tailwindcss(), tsConfigPaths()],
test: {
environment: 'node',
include: ['src/**/*.spec.ts'],
},
build: {
sourcemap: true,
modulePreload: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"collection": "layers_maps",
"field": "menuText",
"type": "string",
"meta": {
"collection": "layers_maps",
"conditions": null,
"display": null,
"display_options": null,
"field": "menuText",
"group": null,
"hidden": false,
"interface": "input",
"note": "Optional: Text im Plus-Menü nur für diese Karte. Leer = menuText vom Layer.",
"options": {
"placeholder": "leer = menuText vom Layer übernehmen"
},
"readonly": false,
"required": false,
"searchable": true,
"sort": 5,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "menuText",
"table": "layers_maps",
"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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"collection": "layers_maps",
"field": "name",
"type": "string",
"meta": {
"collection": "layers_maps",
"conditions": null,
"display": null,
"display_options": null,
"field": "name",
"group": null,
"hidden": false,
"interface": "input",
"note": "Optional: Layer-Name nur für diese Karte (z.B. englisch auf der einen, deutsch auf der anderen). Leer = Name vom Layer.",
"options": {
"placeholder": "leer = Name vom Layer übernehmen"
},
"readonly": false,
"required": false,
"searchable": true,
"sort": 4,
"special": null,
"translations": null,
"validation": null,
"validation_message": null,
"width": "half"
},
"schema": {
"name": "name",
"table": "layers_maps",
"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
}
}
Loading