mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Merge branch 'main' into fix/132-filter-modes
This commit is contained in:
@@ -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: ""
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
export const ENV = 'dev'
|
||||
export const BACKEND_URL = 'http://localhost'
|
||||
export const ENABLE_WRITE = true
|
||||
export const GA4_MEASUREMENT_ID = undefined
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user