diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
index 071f575e5..0db7acf3e 100644
--- a/.github/workflows/frontend.yml
+++ b/.github/workflows/frontend.yml
@@ -22,35 +22,56 @@ defaults:
run:
working-directory: frontend
-# TODO: add a lint job once ESLint migration replaces the (already-removed in
-# @angular-devkit/build-angular v20) `tslint` builder that `yarn lint` calls.
-
jobs:
- test:
+ check:
runs-on: ubuntu-latest
timeout-minutes: 20
- container:
- image: mcr.microsoft.com/playwright:v1.58.1-noble
+ permissions:
+ contents: read
steps:
- uses: actions/checkout@v5
+ with:
+ persist-credentials: false
+ - uses: pnpm/action-setup@v4
+ with:
+ package_json_file: frontend/package.json
- uses: actions/setup-node@v4
with:
node-version: '24'
- cache: 'yarn'
- cache-dependency-path: frontend/yarn.lock
- - run: yarn install --immutable
- - name: Run unit tests
- run: yarn test:ci
+ cache: 'pnpm'
+ cache-dependency-path: frontend/pnpm-lock.yaml
+ - run: pnpm install --frozen-lockfile
+ - name: Lint
+ run: pnpm lint
+ - name: Typecheck
+ run: pnpm typecheck
+ - name: Unit tests
+ run: pnpm test:ci
+ - name: Build (self-hosted)
+ run: pnpm build
+ - name: Build (SaaS)
+ run: pnpm build:saas
+ - name: Build Storybook
+ run: pnpm build-storybook
license:
runs-on: ubuntu-latest
timeout-minutes: 10
+ permissions:
+ contents: read
steps:
- uses: actions/checkout@v5
+ with:
+ persist-credentials: false
+ - uses: pnpm/action-setup@v4
+ with:
+ package_json_file: frontend/package.json
- uses: actions/setup-node@v4
with:
node-version: '24'
- cache: 'yarn'
- cache-dependency-path: frontend/yarn.lock
+ cache: 'pnpm'
+ cache-dependency-path: frontend/pnpm-lock.yaml
+ - run: pnpm install --frozen-lockfile
+ # OFL-1.1: the Geist and Space Grotesk fonts (@fontsource-variable/*).
- name: license checker
- run: 'npx --yes license-checker --onlyAllow="MIT;ISC;Python-2.0;Apache-2.0;BSD;MPL;CC;Custom: http://github.com/dscape/statsd-parser;" --excludePrivatePackages'
+ run: 'npx --yes license-checker --onlyAllow="MIT;ISC;Python-2.0;Apache-2.0;BSD;MPL;CC;OFL-1.1;Custom: http://github.com/dscape/statsd-parser;" --excludePrivatePackages'
diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml
index c6da2770f..298be9cc8 100644
--- a/.github/workflows/storybook.yml
+++ b/.github/workflows/storybook.yml
@@ -20,13 +20,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ package_json_file: frontend/package.json
- uses: actions/setup-node@v4
with:
node-version: '24'
- cache: yarn
- cache-dependency-path: frontend/yarn.lock
- - run: cd frontend && yarn install --frozen-lockfile
- - run: cd frontend && yarn storybook:build
+ cache: pnpm
+ cache-dependency-path: frontend/pnpm-lock.yaml
+ - run: cd frontend && pnpm install --frozen-lockfile
+ - run: cd frontend && pnpm build-storybook
- uses: actions/upload-pages-artifact@v3
with:
path: frontend/storybook-static
diff --git a/.gitignore b/.gitignore
index 3cea27708..38a967a31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,9 +6,12 @@
/out-tsc
# Only exists if Bazel was run
/bazel-out
+/frontend/dist
+/frontend/coverage
# dependencies
/node_modules
+/frontend/node_modules
.yarn/
# yarn berry cache (not tracked at repo root)
diff --git a/AGENTS.md b/AGENTS.md
index 6d4bd644a..61c17f276 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
RocketAdmin is a database administration panel that allows users to manage database connections, tables, and data. It consists of multiple components in a monorepo structure:
- **backend/** - NestJS API server (TypeScript, ES modules)
-- **frontend/** - Angular 19 web application (standalone components)
+- **frontend/** - Vue 3 web application (Vite, Pinia, vue-router)
- **rocketadmin-agent/** - NestJS agent for connecting to databases behind firewalls
- **autoadmin-ws-server/** - WebSocket server for agent communication
- **shared-code/** - Shared data access layer and utilities used by backend and agent
@@ -30,11 +30,13 @@ pnpm test-saas # Run SaaS-specific tests
```bash
cd frontend
-yarn start # Start Angular dev server
-yarn build # Production build
-yarn test:ci # Run tests headlessly (CI mode)
-yarn test --browsers=ChromeHeadlessCustom --no-watch --no-progress # Headless tests
-yarn lint # TSLint (deprecated, needs ESLint migration)
+pnpm install # Installed on its own, not as a workspace member
+pnpm dev # Start Vite dev server (proxies /api)
+pnpm build # Production build (pnpm build:saas for SaaS)
+pnpm lint # biome + eslint-plugin-vue
+pnpm typecheck # vue-tsc
+pnpm test:ci # Vitest (unit and component tests)
+pnpm test:e2e # Playwright against a mocked API
```
### Running Backend Tests with Docker
@@ -84,7 +86,7 @@ pnpm migration:revert # Revert last migration
- Uses pnpm workspaces with packages: `backend`, `rocketadmin-agent`, `shared-code`
- `shared-code` is imported as `@rocketadmin/shared-code` workspace dependency
-- Frontend is a separate Angular project (not a workspace member)
+- Frontend is a separate pnpm project with its own lockfile (not a workspace member)
### Backend (NestJS)
@@ -102,15 +104,16 @@ pnpm migration:revert # Revert last migration
- `saas-tests/` - SaaS-specific feature tests
- `complex-table-tests/` - Complex table operation tests
-### Frontend (Angular 19)
+### Frontend (Vue 3)
-See `frontend/CLAUDE.md` for detailed frontend architecture.
+See `frontend/CLAUDE.md` for detailed frontend architecture and conventions.
Key points:
-- Standalone components (no NgModules)
-- BehaviorSubject-based state management (no NgRx)
-- Multi-environment builds (development, production, saas, saas-production)
-- Jasmine/Karma testing with ChromeHeadless
+- `
+
+
+
+
+
+
+
+
```
-#### 2. Component Template (`your-new-widget.component.html`)
+The shared props (`fieldKey`, `label`, `required`, `readonly`, `disabled`, `structure`, `widgetStructure`, `relations`) come from `RecordEditFieldProps` in `fields/types.ts`; each widget adds its own typed `value`. Note that the field key is `fieldKey` (`key` is reserved in Vue), and the output is the `fieldChange` event. `widgetParams` is the parsed `widget_params` object, whether the API sent it as a string or an object.
-```html
-
- {{normalizedLabel}}
-
-
-```
+#### Optional: Create Other Field Type Components
-#### 3. Component Styles (`your-new-widget.component.css`)
+**Record View Field** (`record-view/YourNewWidgetRecordView.vue`):
+```vue
+
+
+
+ {{ value || '—' }}
+
```
-#### Optional: Create Other Field Type Components
+**Table Display Field** (`table-display/YourNewWidgetDisplay.vue`):
+```vue
+
-**Filter Field** (`filter-fields/your-new-widget/`):
-```typescript
-import { Component } from '@angular/core';
-import { BaseFilterFieldComponent } from '../base-filter-field/base-filter-field.component';
-
-@Component({
- selector: 'app-filter-your-new-widget',
- templateUrl: './your-new-widget.component.html',
- styleUrls: ['./your-new-widget.component.css']
-})
-export class YourNewWidgetFilterComponent extends BaseFilterFieldComponent {
- // Filter logic with operators
-}
+
+ {{ value || '—' }}
+
```
-### Step 3: Register Component (Angular Standalone)
+**Filter Field** (`filter/YourNewWidgetFilter.vue`):
+```vue
+
+
+
+
+
+
+
+```
-### Step 4: Add to Widget Registries
+A filter widget that picks its own comparator (`eq`, `contains`, `startswith`, `empty`, ...) emits `comparatorChange` as well.
-You need to register your widget components in **four separate registry files**, one for each context:
+### Step 3: Add to Widget Registries
-#### 4.1 Record Edit Types Registry
+You need to register your widget components in **four separate registry files** under `frontend/src/consts/`, one for each context. Each file declares the components as lazily loaded `defineAsyncComponent(() => import(...))` constants and maps widget types to them in its `UIwidgets` export:
-**File:** `frontend/src/app/consts/record-edit-types.ts`
+#### 3.1 Record Edit Types Registry
+
+**File:** `frontend/src/consts/record-edit-types.ts`
```typescript
-// Add import at the top
-import { YourNewWidgetEditComponent } from '../components/ui-components/record-edit-fields/your-new-widget/your-new-widget.component';
+const YourNewWidgetEdit = defineAsyncComponent(() => import('@/components/fields/record-edit/YourNewWidgetEdit.vue'));
-// Add to UIwidgets export
export const UIwidgets = {
- // ... existing widgets
- Your_new_widget: YourNewWidgetEditComponent,
-}
+ // ... existing widgets
+ Your_new_widget: YourNewWidgetEdit,
+};
```
-#### 4.2 Record View Types Registry
+The keys of this map also fill the widget type dropdown on the widgets settings page.
+
+#### 3.2 Record View Types Registry
-**File:** `frontend/src/app/consts/record-view-types.ts`
+**File:** `frontend/src/consts/record-view-types.ts`
```typescript
-// Add import at the top
-import { YourNewWidgetRecordViewComponent } from '../components/ui-components/record-view-fields/your-new-widget/your-new-widget.component';
+const YourNewWidgetRecordView = defineAsyncComponent(
+ () => import('@/components/fields/record-view/YourNewWidgetRecordView.vue'),
+);
-// Add to UIwidgets export
export const UIwidgets = {
- // ... existing widgets
- Your_new_widget: YourNewWidgetRecordViewComponent,
-}
+ // ... existing widgets
+ Your_new_widget: YourNewWidgetRecordView,
+};
```
-#### 4.3 Table Display Types Registry
+#### 3.3 Table Display Types Registry
-**File:** `frontend/src/app/consts/table-display-types.ts`
+**File:** `frontend/src/consts/table-display-types.ts`
```typescript
-// Add import at the top
-import { YourNewWidgetDisplayComponent } from '../components/ui-components/table-display-fields/your-new-widget/your-new-widget.component';
+const YourNewWidgetDisplay = defineAsyncComponent(() => import('@/components/fields/table-display/YourNewWidgetDisplay.vue'));
-// Add to UIwidgets export
export const UIwidgets = {
- // ... existing widgets
- Your_new_widget: YourNewWidgetDisplayComponent,
-}
+ // ... existing widgets
+ Your_new_widget: YourNewWidgetDisplay,
+};
```
-#### 4.4 Filter Types Registry
+#### 3.4 Filter Types Registry
-**File:** `frontend/src/app/consts/filter-types.ts`
+**File:** `frontend/src/consts/filter-types.ts`
```typescript
-// Add import at the top
-import { YourNewWidgetFilterComponent } from '../components/ui-components/filter-fields/your-new-widget/your-new-widget.component';
+const YourNewWidgetFilter = defineAsyncComponent(() => import('@/components/fields/filter/YourNewWidgetFilter.vue'));
-// Add to UIwidgets export
export const UIwidgets = {
- // ... existing widgets
- Your_new_widget: YourNewWidgetFilterComponent,
+ // ... existing widgets
+ Your_new_widget: YourNewWidgetFilter,
+};
+```
+
+#### 3.5 Update the Registry Snapshot
+
+`frontend/src/consts/widget-types.spec.ts` compares every map in these four files against `frontend/src/consts/widget-types.angular.json`, a frozen snapshot of the old Angular maps (each entry is the SFC name, e.g. `"Timezone": "TimezoneEdit"`). A new entry fails that test until you add it to the JSON too:
+
+```json
+{
+ "record-edit-types.ts": {
+ "UIwidgets": {
+ "Your_new_widget": "YourNewWidgetEdit"
+ }
+ }
}
```
+Add the matching entry under `record-view-types.ts`, `table-display-types.ts` and `filter-types.ts` for each context you registered.
+
**Important Notes:**
- Each registry file has its own `UIwidgets` export mapping widget types to components
-- If you only created the record-edit-fields component, only add it to `record-edit-types.ts`
+- The same files also hold the DB-type maps (`recordEditTypes`, `recordViewFieldTypes`, `tableDisplayTypes`, `filterTypes`) used for columns without a widget
+- If you only created the record-edit component, only add it to `record-edit-types.ts`
- The system will gracefully fall back to default rendering for missing contexts
-- Component names follow the pattern: `{WidgetName}{Context}Component` where Context is Edit, RecordView, Display, or Filter
+- Component names follow the pattern: `{WidgetName}{Context}.vue` where Context is Edit, RecordView, Display, or Filter
-### Step 5: Add Default Parameters (Optional)
+### Step 4: Add Default Parameters (Optional)
-**File:** `frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts`
+**File:** `frontend/src/components/table/widget-defaults.ts`
-Add default parameters with documentation:
+Add default parameters with documentation to `WIDGET_DEFAULT_PARAMS`. This is the JSON5 text the params editor starts with when the widget type is picked. A type whose default is `NO_SETTINGS` gets a read-only editor. You can also add a docs link to `WIDGET_DOCS_URLS`.
```typescript
-public defaultParams = {
- // ... existing defaults
- Your_new_widget: `{
- // Parameter 1 description
- "param1": "value1",
-
- // Parameter 2 description
- "param2": 100
- }`,
+export const WIDGET_DEFAULT_PARAMS: Record = {
+ // ... existing defaults
+ Your_new_widget: `// Parameter 1 description
+// Parameter 2 description
+{
+ "param1": "value1",
+ "param2": 100
}
+`,
+};
```
-### Step 6: Add Backend Validation (Optional)
+### Step 5: Add Backend Validation (Optional)
**File:** `backend/src/entities/widget/utils/validate-create-widgets-ds.ts`
@@ -556,7 +467,7 @@ async function validateCreateWidgetsDs(widgets, connection, tableName) {
}
```
-### Step 7: Add Documentation
+### Step 6: Add Documentation
Update this file with:
1. Description of the new widget
@@ -564,7 +475,7 @@ Update this file with:
3. Example configuration
4. Supported database types
-### Step 8: Write Tests
+### Step 7: Write Tests
#### Backend Test
@@ -592,38 +503,27 @@ test('should create widget with YOUR_NEW_WIDGET type', async t => {
#### Frontend Test
-**File:** `frontend/src/app/components/ui-components/your-new-widget-edit/your-new-widget-edit.component.spec.ts`
+**File:** `frontend/src/components/fields/record-edit/YourNewWidgetEdit.spec.ts`
```typescript
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { YourNewWidgetEditComponent } from './your-new-widget-edit.component';
-
-describe('YourNewWidgetEditComponent', () => {
- let component: YourNewWidgetEditComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [ YourNewWidgetEditComponent ]
- })
- .compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(YourNewWidgetEditComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('should emit value on change', () => {
- spyOn(component.onFieldChange, 'emit');
- component.handleChange('new value');
- expect(component.onFieldChange.emit).toHaveBeenCalledWith('new value');
- });
+import { mount } from '@vue/test-utils';
+import { describe, expect, it } from 'vitest';
+import { mockWidgetStructure } from '../stories.mocks';
+import YourNewWidgetEdit from './YourNewWidgetEdit.vue';
+
+describe('YourNewWidgetEdit', () => {
+ it('renders the value', () => {
+ const wrapper = mount(YourNewWidgetEdit, { props: { label: 'field', value: 'hello' } });
+ expect(wrapper.find('input').element.value).toBe('hello');
+ });
+
+ it('emits fieldChange on input', async () => {
+ const wrapper = mount(YourNewWidgetEdit, {
+ props: { label: 'field', widgetStructure: mockWidgetStructure({ param1: 'value1' }) },
+ });
+ await wrapper.find('[data-testid="record-field-your-new-widget"]').setValue('new value');
+ expect(wrapper.emitted('fieldChange')).toEqual([['new value']]);
+ });
});
```
@@ -647,13 +547,13 @@ describe('YourNewWidgetEditComponent', () => {
### 3. Retrieval
- Widgets fetched when table is loaded
- API: `GET /widgets/:connectionId?tableName={tableName}`
-- Cached in frontend service
+- Cached in the frontend tables store (`stores/tables.ts`)
### 4. Rendering
- When displaying/editing records:
1. System checks if field has widget configured
- 2. If yes: Instantiates corresponding Angular component
- 3. Passes `field` config and `value` to component
+ 2. If yes: Renders the Vue component registered for its `widget_type` in that context's `UIwidgets` map (a dynamic ``)
+ 3. Passes `value`, `fieldKey`, `structure` and `widgetStructure` as props, and listens for `fieldChange` / `comparatorChange` / `copyToClipboard`
4. Component renders based on `widget_params`
### 5. Updates
@@ -748,14 +648,16 @@ Test files:
Run frontend tests (from project root):
```bash
-cd frontend && yarn test --browsers=ChromeHeadlessCustom --no-watch --no-progress
+cd frontend && pnpm test:ci
```
-Widget component tests located in:
-- `frontend/src/app/components/ui-components/record-edit-fields/*/your-widget.component.spec.ts`
-- `frontend/src/app/components/ui-components/record-view-fields/*/your-widget.component.spec.ts`
-- `frontend/src/app/components/ui-components/table-display-fields/*/your-widget.component.spec.ts`
-- `frontend/src/app/components/ui-components/filter-fields/*/your-widget.component.spec.ts`
+The tests use Vitest with `@vue/test-utils` (`mount`). Widget component tests sit next to each component:
+- `frontend/src/components/fields/record-edit/YourWidgetEdit.spec.ts`
+- `frontend/src/components/fields/record-view/YourWidgetRecordView.spec.ts`
+- `frontend/src/components/fields/table-display/YourWidgetDisplay.spec.ts`
+- `frontend/src/components/fields/filter/YourWidgetFilter.spec.ts`
+
+`frontend/src/consts/widget-types.spec.ts` checks the registries against the frozen Angular snapshot.
---
@@ -770,101 +672,104 @@ The **Timezone widget** is a complete example demonstrating all the concepts cov
Timezone = 'Timezone'
```
-**Frontend Component** (`record-edit-fields/timezone/timezone.component.ts`):
-```typescript
-import { CUSTOM_ELEMENTS_SCHEMA, Component, Input } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { MatFormFieldModule } from '@angular/material/form-field';
-import { MatSelectModule } from '@angular/material/select';
-import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component';
-
-@Component({
- selector: 'app-edit-timezone',
- imports: [CommonModule, FormsModule, MatFormFieldModule, MatSelectModule],
- templateUrl: './timezone.component.html',
- styleUrls: ['./timezone.component.css'],
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
-})
-export class TimezoneEditComponent extends BaseEditFieldComponent {
- @Input() value: string;
- public timezones: { value: string, label: string }[] = [];
-
- ngOnInit(): void {
- super.ngOnInit();
- this.initializeTimezones();
- }
+**Frontend Component** (`frontend/src/components/fields/record-edit/TimezoneEdit.vue`):
+```vue
+
+
+
+
+
+
+
```
**Widget Registration in All Four Registries:**
-1. `consts/record-edit-types.ts:57`:
+1. `frontend/src/consts/record-edit-types.ts:33, 85`:
```typescript
+const TimezoneEdit = defineAsyncComponent(() => import('@/components/fields/record-edit/TimezoneEdit.vue'));
+
export const UIwidgets = {
- // ... other widgets
- Timezone: TimezoneEditComponent,
- // ... other widgets
-}
+ // ... other widgets
+ Timezone: TimezoneEdit,
+ // ... other widgets
+};
```
-2. `consts/record-view-types.ts:52`:
+2. `frontend/src/consts/record-view-types.ts:41, 72`:
```typescript
export const UIwidgets = {
- // ... other widgets
- Timezone: TimezoneRecordViewComponent,
- // ... other widgets
-}
+ // ... other widgets
+ Timezone: TimezoneRecordView,
+ // ... other widgets
+};
```
-3. `consts/table-display-types.ts:50`:
+3. `frontend/src/consts/table-display-types.ts:35, 64`:
```typescript
export const UIwidgets = {
- // ... other widgets
- Timezone: TimezoneDisplayComponent,
- // ... other widgets
-}
+ // ... other widgets
+ Timezone: TimezoneDisplay,
+ // ... other widgets
+};
```
-4. `consts/filter-types.ts:38`:
+4. `frontend/src/consts/filter-types.ts:26, 46`:
```typescript
export const UIwidgets = {
- // ... other widgets
- Timezone: TimezoneFilterComponent,
- // ... other widgets
-}
+ // ... other widgets
+ Timezone: TimezoneFilter,
+ // ... other widgets
+};
```
-**Default Parameters** (`db-table-widgets.component.ts:249-255`):
+Each of these entries is also listed in the frozen snapshot `frontend/src/consts/widget-types.angular.json`.
+
+**Default Parameters** (`frontend/src/components/table/widget-defaults.ts:185-191`):
```typescript
-Timezone: `// Configure timezone widget options
+ Timezone: `// Configure timezone widget options
// Uses Intl API to populate timezone list automatically
// allow_null: Allow empty/null value selection
{
@@ -876,42 +781,41 @@ Timezone: `// Configure timezone widget options
**Key Features:**
- Uses browser's Intl API (`Intl.supportedValuesOf('timeZone')`)
- Automatically calculates UTC offsets for each timezone
-- Supports `allow_null` parameter
-- Material Design dropdown with search capability
+- Supports the `allow_null` parameter, and adds the empty option for nullable columns too
+- `UiSelect` dropdown
- Alphabetically sorted timezone list
+- The record view and table display show the zone with its offset; the table cell copies the value to the clipboard
- Comprehensive test coverage
**Component Files (All Four Contexts):**
+Each context has the component, a Vitest spec and a Storybook story, under `frontend/src/components/fields/`:
+
**Record Edit Fields:**
-- Component: `frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.ts`
-- Template: `frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.html`
-- Styles: `frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.css`
-- Tests: `frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts`
+- Component: `record-edit/TimezoneEdit.vue`
+- Tests: `record-edit/TimezoneEdit.spec.ts`
+- Stories: `record-edit/TimezoneEdit.stories.ts`
**Record View Fields:**
-- Component: `frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.ts`
-- Template: `frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.html`
-- Styles: `frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.css`
-- Tests: `frontend/src/app/components/ui-components/record-view-fields/timezone/timezone.component.spec.ts`
+- Component: `record-view/TimezoneRecordView.vue`
+- Tests: `record-view/TimezoneRecordView.spec.ts`
+- Stories: `record-view/TimezoneRecordView.stories.ts`
**Table Display Fields:**
-- Component: `frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.ts`
-- Template: `frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.html`
-- Styles: `frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.css`
-- Tests: `frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts`
+- Component: `table-display/TimezoneDisplay.vue`
+- Tests: `table-display/TimezoneDisplay.spec.ts`
+- Stories: `table-display/TimezoneDisplay.stories.ts`
**Filter Fields:**
-- Component: `frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.ts`
-- Template: `frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.html`
-- Styles: `frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.css`
-- Tests: `frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.spec.ts`
+- Component: `filter/TimezoneFilter.vue` (its option list comes from `filter/timezones.ts`)
+- Tests: `filter/TimezoneFilter.spec.ts`
+- Stories: `filter/TimezoneFilter.stories.ts`
**Registry Files:**
-- `frontend/src/app/consts/record-edit-types.ts` (line 25, 57)
-- `frontend/src/app/consts/record-view-types.ts` (line 24, 52)
-- `frontend/src/app/consts/table-display-types.ts` (line 24, 50)
-- `frontend/src/app/consts/filter-types.ts` (line 19, 38)
+- `frontend/src/consts/record-edit-types.ts` (line 33, 85)
+- `frontend/src/consts/record-view-types.ts` (line 41, 72)
+- `frontend/src/consts/table-display-types.ts` (line 35, 64)
+- `frontend/src/consts/filter-types.ts` (line 26, 46)
---
@@ -923,13 +827,12 @@ Timezone: `// Configure timezone widget options
- **Table Display Fields:** Used in table rows for listing multiple records (compact display)
- **Filter Fields:** Used in filter panels for searching/filtering table data
-2. **Component Inheritance:** All field components should extend their respective base components:
- - `BaseEditFieldComponent` for record-edit-fields
- - `BaseRecordViewFieldComponent` for record-view-fields
- - `BaseTableDisplayFieldComponent` for table-display-fields
- - `BaseFilterFieldComponent` for filter-fields
+2. **Composables, Not Inheritance:** Field components don't extend a base class. Each one types its props and events with `fields/types.ts` and calls the composable for its context:
+ - `useRecordEditField(props)` for record-edit
+ - `useViewField(props, emit)` for record-view and table-display
+ - `useFilterField(props)` for filter
-3. **Standalone Architecture:** Angular 19 uses standalone components. No NgModule registration needed. Import dependencies directly in component metadata.
+3. **Single-File Components:** Widgets are `