Merge branch 'main' into fix/132-filter-modes

This commit is contained in:
Kari Lintulaakso
2026-04-23 12:38:08 +03:00
committed by GitHub
9 changed files with 120 additions and 3 deletions
+4
View File
@@ -39,3 +39,7 @@ VITE_RUNNING_ENV: dev
VITE_BACKEND_URL: http://localhost:4000
VITE_HMR_PORT: 5173
# Optional: enables Google Analytics (GA4) page-view tracking in the frontend when set.
# Disabled by default, and never runs in `VITE_RUNNING_ENV: dev`.
VITE_GA4_MEASUREMENT_ID: ""
+1 -1
View File
@@ -142,7 +142,7 @@ export const newSpeciesBasis: EditDataType<SpeciesDetailsType & EditMetaData> =
references: references,
}
export const cloneSpeciesData = () => {
export const cloneSpeciesData = (): EditDataType<SpeciesDetailsType & EditMetaData> => {
const cloneReferences = newSpeciesBasis.references?.map(reference => ({
...reference,
ref_journal: reference.ref_journal ? { ...reference.ref_journal } : undefined,
@@ -1,6 +1,7 @@
import { beforeEach, beforeAll, afterAll, describe, it, expect } from '@jest/globals'
import { login, logout, resetDatabase, send, resetDatabaseTimeout } from '../utils'
import { pool } from '../../utils/db'
import { cloneSpeciesData } from './data'
describe('Deleting a species works', () => {
beforeAll(async () => {
@@ -27,6 +28,30 @@ describe('Deleting a species works', () => {
expect(getResult.status).toEqual(404) // 'Species response status was not 404 after deletion'
})
it('Deleting succeeds for a species that has synonyms', async () => {
const speciesPayload = cloneSpeciesData()
speciesPayload.species_name = 'delete-with-synonyms-test'
speciesPayload.com_taxa_synonym = [
{
rowState: 'new',
syn_genus_name: 'Pseudodryomys',
syn_species_name: 'simplicidens',
syn_comment: 'test synonym',
},
]
const { body: createBody, status: createStatus } = await send<{ species_id: number }>('species', 'PUT', {
species: { ...speciesPayload, comment: 'create species for delete synonym test' },
})
expect(createStatus).toEqual(200)
expect(typeof createBody.species_id).toEqual('number')
const deleteResult = await send<{ id: number }>(`species/${createBody.species_id}`, 'DELETE')
expect(deleteResult.status).toEqual(200)
const getResult = await send(`species/${createBody.species_id}`, 'GET')
expect(getResult.status).toEqual(404)
})
it('Deleting fails without permissions', async () => {
logout()
const deleteResultNoPerm = await send<{ id: number }>('species/21052', 'DELETE')
+16 -1
View File
@@ -7,11 +7,26 @@ import { getSpeciesDetails } from '../species'
import { makeListRemoved } from './writeOperations/utils'
const getSpeciesWriteHandler = (type: ActionType) => {
const baseAllowedColumns = getFieldsOfTables([
'com_species',
'now_ls',
'now_sau',
'now_sr',
'now_lau',
'com_taxa_synonym',
])
// Defensive: `com_taxa_synonym.synonym_id` is required for delete/update operations when a species has synonyms.
// We've observed runtime failures complaining it is "Non-allowed", so ensure it is always present.
const allowedColumns = Array.from(
new Set([...baseAllowedColumns, 'synonym_id', 'species_id', 'syn_genus_name', 'syn_species_name', 'syn_comment'])
)
return new WriteHandler({
dbName: NOW_DB_NAME,
table: 'com_species',
idColumn: 'species_id',
allowedColumns: getFieldsOfTables(['com_species', 'now_ls', 'now_sau', 'now_sr', 'now_lau', 'com_taxa_synonym']),
allowedColumns,
type,
})
}
+7
View File
@@ -67,6 +67,13 @@ Some DB fields use MariaDB `DECIMAL` and are represented as `Decimal` in Prisma-
- Prefer explicit parsing/conversion at usage points if numeric operations are required.
- `estimate_temp` additionally has validator bounds to avoid DB errors from out-of-range values (`-999.9` to `999.9`).
**Analytics (optional)**
The frontend can optionally send GA4 page-view events if configured.
- Set `VITE_GA4_MEASUREMENT_ID` to your GA4 measurement id (for example `G-XXXXXXXXXX`).
- Tracking is disabled by default and never runs when `VITE_RUNNING_ENV` is `dev`.
**Detailed documentation for editingComponents**
- [RadioSelector](./components/editingComponents/RadioSelector.md)
+9 -1
View File
@@ -1,11 +1,19 @@
import { Container } from '@mui/material'
import Grid from '@mui/material/Grid2'
import { Outlet } from 'react-router-dom'
import { Outlet, useLocation } from 'react-router-dom'
import { NavBar } from './components/NavBar'
import { Notification, NotificationContextProvider } from './components/Notification'
import { Footer } from './Footer'
import { useEffect } from 'react'
import { trackPageView } from './util/analytics'
const App = () => {
const location = useLocation()
useEffect(() => {
trackPageView(`${location.pathname}${location.search}`)
}, [location.pathname, location.search])
return (
<NotificationContextProvider>
<Notification />
+1
View File
@@ -1,3 +1,4 @@
export const ENV = 'dev'
export const BACKEND_URL = 'http://localhost'
export const ENABLE_WRITE = true
export const GA4_MEASUREMENT_ID = undefined
+56
View File
@@ -0,0 +1,56 @@
import { ENV, GA4_MEASUREMENT_ID } from './config'
declare global {
interface Window {
dataLayer?: unknown[]
gtag?: (...args: unknown[]) => void
}
}
const canTrack = () => {
if (ENV === 'dev') return false
if (!GA4_MEASUREMENT_ID) return false
// Honor "Do Not Track" signals when present.
const win = window as Window & { doNotTrack?: string }
const dnt = navigator.doNotTrack === '1' || win.doNotTrack === '1'
if (dnt) return false
return true
}
let initialized = false
const ensureGtagLoaded = () => {
if (initialized) return
if (!canTrack()) return
if (typeof document === 'undefined') return
const srcId = 'ga4-gtag-src'
if (!document.getElementById(srcId)) {
const script = document.createElement('script')
script.id = srcId
script.async = true
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(GA4_MEASUREMENT_ID!)}`
document.head.appendChild(script)
}
window.dataLayer = window.dataLayer ?? []
window.gtag =
window.gtag ??
((...args: unknown[]) => {
window.dataLayer?.push(args)
})
window.gtag('js', new Date())
window.gtag('config', GA4_MEASUREMENT_ID, { send_page_view: false, anonymize_ip: true })
initialized = true
}
export const trackPageView = (path: string) => {
if (!canTrack()) return
ensureGtagLoaded()
window.gtag?.('event', 'page_view', { page_path: path })
}
+1
View File
@@ -1,3 +1,4 @@
export const ENV = import.meta.env.VITE_RUNNING_ENV as 'dev' | 'staging' | 'prod'
export const BACKEND_URL = import.meta.env.VITE_BACKEND_URL as string
export const ENABLE_WRITE = import.meta.env.VITE_ENABLE_WRITE === 'true'
export const GA4_MEASUREMENT_ID = import.meta.env.VITE_GA4_MEASUREMENT_ID as string | undefined