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 + + + ``` -#### 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 -} + ``` -### 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 ` diff --git a/frontend/src/index.saas.html b/frontend/index.saas.html similarity index 99% rename from frontend/src/index.saas.html rename to frontend/index.saas.html index 6d4db281f..8c2ee6913 100644 --- a/frontend/src/index.saas.html +++ b/frontend/index.saas.html @@ -142,7 +142,7 @@ - +