u
This commit is contained in:
@@ -2,275 +2,10 @@
|
|||||||
name: coding-style
|
name: coding-style
|
||||||
description: >-
|
description: >-
|
||||||
Coding and project style conventions for the Dashfront repository.
|
Coding and project style conventions for the Dashfront repository.
|
||||||
Use this skill whenever authoring, modifying, or reviewing code, creating or updating
|
|
||||||
automats, writing React PureComponents, integrating dashgui components, or structuring
|
|
||||||
backend API and SSE communication. Names src/automats/brandAutomat.js as the canonical reference automat.
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Dashfront Coding & Project Style Guide
|
class componets disconnected from state, in automat.
|
||||||
|
|
||||||
This skill defines the canonical coding standards, architectural patterns, and project conventions for the `dashfront` codebase.
|
Reference:
|
||||||
|
Component: src/routes/artikel/marken.jsx
|
||||||
> **Canonical Reference Automat**: [src/automats/brandAutomat.js](file:///home/seb/src/dashfront/src/automats/brandAutomat.js)
|
it's Automat: src/automats/brandAutomat.js
|
||||||
> **Canonical Reference View**: [src/routes/artikel/marken.jsx](file:///home/seb/src/dashfront/src/routes/artikel/marken.jsx)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Core Architectural Principles
|
|
||||||
|
|
||||||
1. **State Lives Outside React**: Domain and configuration state lives in module-scoped `react-automat` stores (`src/automats/*`), independent of component lifecycles.
|
|
||||||
2. **Zero Blank Flash**: Components read initial state **synchronously** in the `constructor` from the automat before subscribing in `componentDidMount`. Never show empty flicker when data is already cached.
|
|
||||||
3. **PureComponents Over Hooks for Stateful Views**: Views and widgets subscribe directly via `automat.subscribe(this, selector)` in React `PureComponent` classes. Do **not** use React hooks (`useState`, `useEffect`) inside `PureComponent` classes.
|
|
||||||
4. **No React Context for Data**: Never create React Context providers for domain models. Components import the automat or `src/api` namespace directly.
|
|
||||||
5. **No Mock Backends**: Never mock API routes or fake endpoints. The real Express backend runs locally on port `4208` (`dashback`), proxied via Vite.
|
|
||||||
6. **Silent SSE Updates**: Real-time server-sent events trigger **silent reloads** (`actions.reload()`), preserving visible UI state without resetting to `'loading'`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Canonical Reference Automat: `brandAutomat.js`
|
|
||||||
|
|
||||||
All configuration and list automats in `src/automats/` must follow the structure and conventions exemplified by [src/automats/brandAutomat.js](file:///home/seb/src/dashfront/src/automats/brandAutomat.js):
|
|
||||||
|
|
||||||
```
|
|
||||||
src/automats/brandAutomat.js
|
|
||||||
├── 1. File JSDoc with Upstream Triggers documentation
|
|
||||||
├── 2. Imports (react-automat, api, authAutomat, sseAutomat)
|
|
||||||
├── 3. JSDoc State Typedef & initialState
|
|
||||||
├── 4. Pure Helper Functions (sorting, filtering)
|
|
||||||
├── 5. Concurrency control (inflightLoad guard, pagination)
|
|
||||||
├── 6. State Updater Helper (updateStateWithBrands)
|
|
||||||
├── 7. Actions Object (filter, load, reload, CRUD, reset)
|
|
||||||
├── 8. Automat Singleton Instantiation (new Automat(initialState, actions, { name }))
|
|
||||||
├── 9. Upstream Subscriptions (authAutomat lifecycle, sseAutomat events)
|
|
||||||
└── 10. Default Export
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Patterns Illustrated by `brandAutomat.js`
|
|
||||||
|
|
||||||
#### A. Upstream Triggers Documentation
|
|
||||||
Always document the trigger contract at the top of the file:
|
|
||||||
```js
|
|
||||||
/**
|
|
||||||
* @file brandAutomat.js
|
|
||||||
* @description Manages catalog brands and manufacturers for the active company.
|
|
||||||
*
|
|
||||||
* Upstream Triggers:
|
|
||||||
* - `authAutomat`: Subscribes to auth state changes — calls `actions.load()` on
|
|
||||||
* `'authed'` with a `selectedCompanyId`, and `actions.reset()` on `'anon'`.
|
|
||||||
* - `sseAutomat`: Subscribes to the single `'brand'` event carrying
|
|
||||||
* `{ sender, brandId }` — any create/rename/delete/picture change triggers
|
|
||||||
* `actions.reload()`, which silently re-fetches the list. Events sent by this tab
|
|
||||||
* (matching `sender`) are ignored.
|
|
||||||
* - UI / Settings: Mutating actions (`create`, `rename`, `remove`, `addImage`, `deleteImage`).
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
#### B. Predictable State Shape & Status Progression
|
|
||||||
Define state via JSDoc `@typedef` with standardized status progression:
|
|
||||||
```js
|
|
||||||
/**
|
|
||||||
* @typedef {Object} BrandState
|
|
||||||
* @property {Array<object>} brands All items (full list, decoupled from page UI).
|
|
||||||
* @property {Array<object>} filteredBrands Filtered items matching active filter.
|
|
||||||
* @property {string} filter Active search/filter string.
|
|
||||||
* @property {number} total Unpaged count.
|
|
||||||
* @property {'idle'|'loading'|'succeeded'|'failed'} status
|
|
||||||
* @property {string|null} error
|
|
||||||
*/
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
brands: [],
|
|
||||||
filteredBrands: [],
|
|
||||||
filter: '',
|
|
||||||
total: 0,
|
|
||||||
status: 'idle',
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### C. Inflight Load Deduplication & Silent Reload
|
|
||||||
- Guard fetches with `let inflightLoad = null` to coalesce concurrent load/reload calls into a single HTTP promise.
|
|
||||||
- `load()` sets `status: 'loading'` for initial loads.
|
|
||||||
- `reload()` silently updates state without setting `status: 'loading'`, keeping existing data on screen to prevent flicker.
|
|
||||||
|
|
||||||
```js
|
|
||||||
let inflightLoad = null;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
/** Initial load with visible loading state */
|
|
||||||
async load() {
|
|
||||||
if (inflightLoad) return inflightLoad;
|
|
||||||
brandAutomat.setState({ status: 'loading', error: null });
|
|
||||||
return (inflightLoad = fetchAllBrands()
|
|
||||||
.then(({ brands }) => updateStateWithBrands(brands, { status: 'succeeded' }))
|
|
||||||
.catch((err) => {
|
|
||||||
brandAutomat.setState({ status: 'failed', error: err?.message ?? String(err) });
|
|
||||||
throw err;
|
|
||||||
})
|
|
||||||
.finally(() => { inflightLoad = null; }));
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Silent re-fetch on SSE signal or background sync */
|
|
||||||
async reload() {
|
|
||||||
if (inflightLoad) return inflightLoad;
|
|
||||||
return (inflightLoad = fetchAllBrands()
|
|
||||||
.then(({ brands }) => updateStateWithBrands(brands, { status: 'succeeded', error: null }))
|
|
||||||
.catch((err) => {
|
|
||||||
brandAutomat.setState({ error: err?.message ?? String(err) });
|
|
||||||
throw err;
|
|
||||||
})
|
|
||||||
.finally(() => { inflightLoad = null; }));
|
|
||||||
},
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
brandAutomat.setState(initialState);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### D. Auth Lifecycle Coordination
|
|
||||||
Automatically tie the automat lifecycle to `authAutomat`:
|
|
||||||
```js
|
|
||||||
authAutomat.subscribe((auth) => {
|
|
||||||
const companyId = auth.user?.selectedCompanyId;
|
|
||||||
if (auth.status === 'authed' && companyId) void brandAutomat.actions.load();
|
|
||||||
else if (auth.status === 'anon') brandAutomat.actions.reset();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### E. SSE Echo Suppression
|
|
||||||
When handling backend SSE events, always verify `sender` against `getSseClientId()` to suppress redundant reloads for mutations dispatched by the current browser tab:
|
|
||||||
```js
|
|
||||||
sseAutomat.actions.on('brand', (event) => {
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(event?.data ?? '{}');
|
|
||||||
if (payload?.sender && payload.sender === getSseClientId()) return;
|
|
||||||
} catch { }
|
|
||||||
void brandAutomat.actions.reload().catch(() => { });
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Reference UI Component Pattern
|
|
||||||
|
|
||||||
Components consuming automats must follow the pattern established in [src/routes/artikel/marken.jsx](file:///home/seb/src/dashfront/src/routes/artikel/marken.jsx):
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
import brandAutomat from '../../automats/brandAutomat';
|
|
||||||
import { notifyError } from '../../utils/notify';
|
|
||||||
import { enqueueSnackbar } from 'notistack';
|
|
||||||
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
|
||||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
|
||||||
|
|
||||||
export class ArtikelMarkenSearchField extends PureComponent {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
// 1. Synchronous initial read — zero blank flash!
|
|
||||||
this.state = {
|
|
||||||
input: brandAutomat.state.filter ?? '',
|
|
||||||
submitting: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// 2. Fine-grained selector subscription
|
|
||||||
this.unsub = brandAutomat.subscribe(this, (state) => ({
|
|
||||||
input: state.filter,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// 3. Always clean up subscription
|
|
||||||
this.unsub?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
onInputChange(input) {
|
|
||||||
this.setState({ input });
|
|
||||||
brandAutomat.actions.filter(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleSubmit() {
|
|
||||||
const name = this.state.input.trim();
|
|
||||||
if (!name || this.state.submitting) return;
|
|
||||||
|
|
||||||
this.setState({ submitting: true });
|
|
||||||
try {
|
|
||||||
const row = await brandAutomat.actions.create({ name });
|
|
||||||
enqueueSnackbar(`Marke „${row.name || name}“ angelegt.`, { variant: 'success' });
|
|
||||||
this.setState({ input: '', submitting: false });
|
|
||||||
brandAutomat.actions.filter('');
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ submitting: false });
|
|
||||||
notifyError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { input, submitting } = this.state;
|
|
||||||
return (
|
|
||||||
<SingleLineTextEdit
|
|
||||||
value={input}
|
|
||||||
disabled={submitting}
|
|
||||||
onChange={(next) => this.onInputChange(next)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Component Rules
|
|
||||||
1. **Always unsubscribe**: Save the return value of `automat.subscribe(this, ...)` in `this.unsub` and invoke `this.unsub?.()` in `componentWillUnmount()`.
|
|
||||||
2. **Synchronous constructor reads**: Read directly from `automat.state` or helper selector (e.g. `getArticle(sku)`) in the constructor.
|
|
||||||
3. **Selector efficiency**: The second argument to `subscribe(this, selector)` should return an object with only the slice of state needed by this component.
|
|
||||||
4. **No hooks in PureComponents**: Never call `useState`, `useEffect`, or `useMemo` inside `PureComponent`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. UI Library Integration (`dashgui` & MUI)
|
|
||||||
|
|
||||||
1. **Leverage `dashgui` Components**:
|
|
||||||
- `PanelTile` & `PanelTileGrid`: Container cards and responsive layouts.
|
|
||||||
- `SingleLineTextEdit`: Editable single-line text inputs (supports `search`, `clear`, `size="small"`).
|
|
||||||
- `RecordList`: High-performance list view for tabular and recordset data.
|
|
||||||
- `ElevatedPill`: Badges, chips, and status pills.
|
|
||||||
- `FillSpace`: Layout spacer.
|
|
||||||
2. **Theme Tokens & Color Constants**:
|
|
||||||
- Import shared palette constants from [src/colors.js](file:///home/seb/src/dashfront/src/colors.js) (`MAIN_BACKGROUND`, `PURPLE_PANEL`, etc.).
|
|
||||||
- Use standard MUI `Box`, `Button`, `Typography`, `CircularProgress` with `sx` props for layout spacing (`gap: 1`, `pb: 2`).
|
|
||||||
3. **Notifications**:
|
|
||||||
- Success: `enqueueSnackbar('Erfolgreich gespeichert', { variant: 'success' })` from `notistack`.
|
|
||||||
- Error: `notifyError(err)` from `src/utils/notify`.
|
|
||||||
- Page/Section Error Display: `<BackendError error={...} />`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Backend Communication & Pricing Rules
|
|
||||||
|
|
||||||
1. **API Namespaces**:
|
|
||||||
- Always import from `src/api` (`import { brand, article, delivery } from '../api'`).
|
|
||||||
- The `http()` client automatically sets `credentials: 'include'`.
|
|
||||||
- **Never pass `googleAuthId`** in request payloads; the backend extracts identity directly from the HTTP-only cookie.
|
|
||||||
2. **Pricing Convention**:
|
|
||||||
- Prices sent to and received from the API/database are **net prices** (`net`).
|
|
||||||
- Gross prices displayed to end users must be calculated using tax rates from `companyAutomat` (`selectCompanyTaxrate()`, `selectCompanyReducedTaxrate()`).
|
|
||||||
3. **Binary / Media Assets**:
|
|
||||||
- Compress images client-side into AVIF format (`@jsquash/avif`) before uploading.
|
|
||||||
- Use `ThumbHash` for instant blur placeholders before loading full images from `/api/thumb/{hash}` or `/api/image/{hash}`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Implementation Checklist for New Automats
|
|
||||||
|
|
||||||
When adding a new domain or configuration automat:
|
|
||||||
|
|
||||||
- [ ] Place file in [src/automats/](file:///home/seb/src/dashfront/src/automats/).
|
|
||||||
- [ ] Add JSDoc file header documenting **Upstream Triggers** (auth, SSE, UI).
|
|
||||||
- [ ] Model the state with JSDoc `@typedef`, including `status: 'idle'|'loading'|'succeeded'|'failed'` and `error`.
|
|
||||||
- [ ] Implement `inflightLoad` promise guard for request deduplication.
|
|
||||||
- [ ] Implement both `load()` (visible loading) and `reload()` (silent background refresh).
|
|
||||||
- [ ] Subscribe to `authAutomat` to auto-load on `'authed'` with `selectedCompanyId` and `reset()` on `'anon'`.
|
|
||||||
- [ ] Subscribe to `sseAutomat` with `getSseClientId()` echo check to trigger `actions.reload()`.
|
|
||||||
- [ ] Instantiate `new Automat(initialState, actions, { name: '...' })`.
|
|
||||||
- [ ] Export singleton as default and re-export in [src/automats/index.js](file:///home/seb/src/dashfront/src/automats/index.js).
|
|
||||||
Reference in New Issue
Block a user