From daf86edf1dc17da8eb984d9422423743b6a28fe6 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Thu, 21 May 2026 17:07:17 +0300 Subject: [PATCH 01/11] feat(table-view): mobile cards, bottom-sheet row preview, sort/filters cleanup - Table view: card-style rows on mobile (rounded border, dashed inter-cell separators, label/value centered between dashes, checkbox+actions strip non-clickable for preview), unified icon button styling, mobile-only Sort button on the saved-filters row. - Saved filters: combined dropdown trigger on mobile (active filter shown with check + bold name, "New fast filter" entry at the bottom); desktop chip layout preserved. Applied-filter details get a soft background and no longer trigger horizontal overflow. - Row preview: bottom sheet on mobile with backdrop scrim, drag handle, safe-area padding. Related records flattened (outer expansion removed) and per-row navigation moved to an explicit chevron button to prevent accidental teleports. - Mobile actions: icon-only with tooltips, unified outlined symbol set, count badge hidden when label collapses. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../db-table-row-view.component.css | 95 ++++++++-- .../db-table-row-view.component.html | 33 ++-- .../db-table-view/db-table-view.component.css | 169 ++++++++++++++++-- .../db-table-view.component.html | 101 +++++++---- .../saved-filters-panel.component.css | 168 ++++++++++++++++- .../saved-filters-panel.component.html | 44 ++++- 6 files changed, 534 insertions(+), 76 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css index 37d801447..575bc3eb7 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css @@ -30,13 +30,72 @@ @media (width <= 600px) { .row-preview-sidebar_open { position: fixed; - top: 44px; + top: auto; bottom: 0; left: 0; + right: 0; min-height: initial; - max-height: initial; + max-height: calc(80vh + env(safe-area-inset-bottom, 0px)); width: 100vw; - z-index: 2; + z-index: 1001; + border-top-left-radius: 16px; + border-top-right-radius: 16px; + border-left: none !important; + box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18); + padding-bottom: env(safe-area-inset-bottom, 0px); + animation: rowPreviewSlideUp 240ms cubic-bezier(0.2, 0.8, 0.2, 1); + } +} + +.row-preview-backdrop { + display: none; +} + +.row-preview-sidebar__handle { + display: none; +} + +@media (width <= 600px) { + .row-preview-backdrop { + display: block; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 1000; + animation: rowPreviewBackdropFadeIn 240ms ease-out; + } + + .row-preview-sidebar__handle { + display: block; + width: 36px; + height: 4px; + border-radius: 2px; + background: rgba(0, 0, 0, 0.2); + margin: 8px auto 0; + } +} + +@media (prefers-color-scheme: dark) { + .row-preview-sidebar__handle { + background: rgba(255, 255, 255, 0.24); + } +} + +@keyframes rowPreviewSlideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +@keyframes rowPreviewBackdropFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; } } @@ -88,18 +147,18 @@ width: 100%; } -.related-records-panel { - margin-left: 4px; - width: calc(100% - 8px); -} - -.related-records-panel__header { - height: 36px !important; - padding: 0 12px 0 8px; +.related-records-section { + margin: 8px 4px 0; } -.related-records-panel ::ng-deep .mat-expansion-panel-body { - padding: 0; +.related-records-section__title { + margin: 0 0 4px; + padding: 0 12px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + opacity: 0.6; } .related-records__header { @@ -122,6 +181,16 @@ padding-right: 8px; } +.related-record__open { + color: rgba(0, 0, 0, 0.54); +} + +@media (prefers-color-scheme: dark) { + .related-record__open { + color: rgba(255, 255, 255, 0.7); + } +} + .related-record ::ng-deep .mdc-list-item__primary-text::before { height: 24px; } diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html index d8a41175c..572a39908 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html @@ -1,6 +1,10 @@ +
+

Preview

@@ -23,10 +27,8 @@

Preview

- - - Related records - +
diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css index 13184e2a3..426fb45b5 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css @@ -50,8 +50,57 @@ @media (width <= 600px) { .table-switcher { - display: initial; - margin-top: 12px; + display: inline-flex; + align-items: center; + padding: 0 4px 0 8px; + line-height: 1.2; + min-height: 40px; + --mdc-text-button-label-text-size: 20px; + --mdc-text-button-label-text-weight: 500; + --mat-text-button-label-text-size: 20px; + --mat-text-button-label-text-weight: 500; + } + + .table-switcher ::ng-deep .mdc-button__label { + font-size: 20px; + font-weight: 500; + } + + .table-switcher__name { + max-width: 60vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 20px; + font-weight: 500; + } + + .table-switcher__chevron { + margin-left: 0 !important; + font-size: 24px; + width: 24px; + height: 24px; + } +} + +.table-switcher-menu__group-label { + padding: 8px 16px 4px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.6; + pointer-events: none; +} + +.table-switcher-menu__item_active { + font-weight: 600; + background: rgba(0, 0, 0, 0.04); +} + +@media (prefers-color-scheme: dark) { + .table-switcher-menu__item_active { + background: rgba(255, 255, 255, 0.08); } } @@ -133,11 +182,82 @@ margin-top: -16px; } +.saved-filters-row { + display: contents; +} + +.mobile-sort-button { + display: none !important; +} + @media (width <= 600px) { - .actions { - flex-wrap: wrap; + .saved-filters-row { + display: flex; + align-items: flex-start; gap: 8px; } + + .saved-filters-row > app-saved-filters-panel { + flex: 1 1 auto; + min-width: 0; + } + + .mobile-sort-button { + display: inline-flex !important; + align-items: center; + gap: 4px; + flex: 0 0 auto; + min-height: 36px; + padding: 0 10px; + font-size: 13px; + font-weight: 500; + } + + .mobile-sort-button__label { + white-space: nowrap; + } + + .mobile-sort-button_active { + color: var(--color-accentedPalette-700); + } + + .actions { + flex-wrap: nowrap; + gap: 0; + width: 100%; + justify-content: flex-end; + margin-top: 0; + } + + .actions .action-label { + display: none; + } + + .actions .db-table-manage-columns-button__count { + display: none; + } + + .actions .mat-mdc-button { + --mdc-text-button-container-height: 40px; + min-width: 40px; + padding: 0 8px; + } + + .actions .mat-mdc-button .mat-icon { + margin: 0 !important; + } + + .ai-insights-button { + margin-left: 0; + } +} + +@media (prefers-color-scheme: dark) and (width <= 600px) { + .mobile-sort-button_active { + background: var(--color-accentedPalette-900); + border-color: var(--color-accentedPalette-600) !important; + color: var(--color-accentedPalette-100); + } } .action_active { @@ -329,12 +449,24 @@ @media (width <= 600px) { .db-table { grid-template-columns: minmax(0, 120px) 1fr !important; + background: transparent !important; } .db-table ::ng-deep tbody { display: grid; grid-template-columns: subgrid; grid-column: 1 / 3; + row-gap: 10px; + background: transparent; + } + + .table-surface { + background: transparent !important; + box-shadow: none !important; + } + + .table-box { + background: transparent; } } @@ -401,12 +533,11 @@ display: grid; grid-template-columns: subgrid; grid-column: 1 / 3; - grid-gap: 12px 28px; - border-bottom-color: var(--mat-table-row-item-outline-color, rgba(0, 0, 0, 0.12)); - border-bottom-width: var(--mat-table-row-item-outline-width, 1px); - border-bottom-style: solid; + grid-gap: 4px 28px; + border: 1px solid var(--mat-table-row-item-outline-color, rgba(0, 0, 0, 0.12)) !important; + border-radius: 10px; height: auto; - padding: 8px 0; + padding: 12px 12px 12px 16px; } .db-table-header-row { @@ -429,8 +560,17 @@ display: grid; grid-template-columns: subgrid; grid-column: 1 / 3; - align-items: flex-start; + align-items: center; + border-bottom: 1px dashed rgba(0, 0, 0, 0.12); + padding-left: 0 !important; + padding-right: 0 !important; + padding-top: 2px; + padding-bottom: 2px; + } + + .db-table-cell:last-of-type { border-bottom: none; + padding-bottom: 0; } .db-table-cell::before { @@ -441,6 +581,12 @@ } } +@media (prefers-color-scheme: dark) and (width <= 600px) { + .db-table-cell { + border-bottom-color: rgba(255, 255, 255, 0.12); + } +} + .db-table ::ng-deep .db-table-cell:first-of-type, .db-table ::ng-deep .mat-mdc-header-cell:first-of-type, .db-table ::ng-deep .mat-mdc-footer-cell:first-of-type { @@ -452,7 +598,8 @@ .db-table ::ng-deep .db-table-cell:first-of-type, .db-table ::ng-deep .mat-mdc-header-cell:first-of-type, .db-table ::ng-deep .mat-mdc-footer-cell:first-of-type { - border-bottom: none; + padding-left: 0; + margin-top: 8px; } /* .db-table-cell { diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 7670a1746..629b054c3 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -2,23 +2,28 @@

{{ displayName }}

- - Table - - - - - {{table.normalizedTableName || table.display_name || table.table}} - - - - - - {{table.normalizedTableName || table.display_name || table.table}} - - - - + + + +
{{ folder.category_name }}
+ +
+ +
Others
+ +
+
- add - Add row + add + Add row +
- +
+ + + + + + + + +
@@ -235,7 +262,8 @@

{{ displayName }}

[aria-label]="checkboxLabel()"> - + {{ displayName }} Actions - + + @@ -38,6 +39,47 @@ + + + + + + + + + +
diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css index 8b45b57f1..bf6c48441 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css @@ -555,9 +555,32 @@ } .filters-form { - flex: 1 0 auto; + flex: 1 1 auto; display: flex; flex-direction: column; + min-height: 0; +} + +.filters-content { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; +} + +::ng-deep .filters-form mat-dialog-actions { + flex: 0 0 auto; + position: sticky; + bottom: 0; + background: var(--mat-dialog-container-color, #fff); + z-index: 2; + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + +@media (prefers-color-scheme: dark) { + ::ng-deep .filters-form mat-dialog-actions { + background: var(--mat-sidenav-content-background-color); + border-top-color: rgba(255, 255, 255, 0.08); + } } .filters-header-description { @@ -628,6 +651,263 @@ line-height: 18px; } +.filters-header { + margin-top: 16px; + margin-bottom: 12px; + min-height: 0 !important; + line-height: 1.2 !important; +} + +.condition-card { + grid-column: 1 / -1; + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px 0; + border-bottom: 1px dashed rgba(0, 0, 0, 0.12); +} + +.condition-card:last-of-type { + border-bottom: none; +} + +@media (prefers-color-scheme: dark) { + .condition-card { + border-bottom-color: rgba(255, 255, 255, 0.12); + } +} + +.condition-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.condition-card__column-name { + font-size: 12px; + font-weight: 600; + opacity: 0.6; +} + +.condition-card .filter-delete-button { + grid-column: auto !important; + margin: 0 !important; + width: 32px !important; + height: 32px !important; + flex: 0 0 32px; + opacity: 0.7; +} + +.condition-card .filter-delete-button:hover { + opacity: 1; +} + +.condition-card .filter-delete-button ::ng-deep .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + line-height: 18px; +} + +.condition-card__inputs { + display: flex; + gap: 4px; + align-items: flex-start; + flex-wrap: wrap; +} + +.condition-card__inputs ::ng-deep .mat-mdc-form-field:not(.comparator-select-field) .mat-mdc-text-field-wrapper { + padding-left: 12px !important; + padding-right: 12px !important; +} + +.condition-card__inputs ::ng-deep .mat-mdc-form-field-subscript-wrapper { + display: none; +} + +.condition-card__inputs > * { + flex: 1 1 0; + min-width: 0; +} + +.condition-card__inputs > .comparator-select-field { + flex: 0 0 56px !important; +} + +.condition-card__value { + display: block; + width: 100%; + min-width: 0; +} + +.condition-card__value ::ng-deep .mat-mdc-form-field { + width: 100% !important; + display: block !important; +} + +.condition-card__value ::ng-deep .full-width { + width: 100% !important; +} + +.condition-card__inputs .comparator-select-field { + flex: 0 0 56px !important; + width: 56px !important; + min-width: 0 !important; + max-width: 56px !important; +} + +.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-text-field-wrapper { + min-width: 0 !important; + width: 56px !important; + padding-left: 8px !important; + padding-right: 4px !important; +} + +.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-flex { + min-width: 0 !important; +} + +.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-infix { + min-width: 0 !important; + width: auto !important; + padding-left: 0 !important; + padding-right: 0 !important; +} + +.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-select-arrow-wrapper { + padding-left: 0; +} + +.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-subscript-wrapper { + display: none !important; +} + +.condition-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding-top: 4px; +} + +.condition-card__editable-label { + font-size: 12px; + opacity: 0.65; +} + +.condition-card__help-icon { + font-size: 16px; + width: 16px; + height: 16px; + cursor: pointer; + opacity: 0.5; + margin-right: auto; +} + +.condition-card__help-icon:hover { + opacity: 0.85; +} + +.condition-card .quick-edit-toggle { + flex: 0 0 auto; +} + +.filters-header::before { + height: 0 !important; + content: none !important; +} + +.filters-header__text { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + line-height: 1.2; + text-align: left; +} + +.filters-header__label { + font-size: 18px; + font-weight: 600; +} + +.filters-header__table { + font-size: 12px; + font-weight: 500; + opacity: 0.6; +} + +@media (width <= 600px) { + .filters-content { + grid-template-columns: 32px 1fr auto !important; + grid-row-gap: 0 !important; + align-items: start !important; + padding-left: 12px !important; + padding-right: 12px !important; + } + + .filters-select { + grid-column: 1 / -1 !important; + } + + .filter-line { + grid-column: 2 / span 2 !important; + flex-direction: column; + gap: 4px; + padding-right: 0; + } + + .filter-line > .column-name { + font-size: 12px; + font-weight: 600; + opacity: 0.6; + margin: 0; + flex: 1 1 100%; + align-self: flex-start; + } + + .filter-line > div, + .filter-line > ndc-dynamic, + .filter-line > ng-template { + flex: 1 1 100%; + width: 100%; + } + + .column-name { + font-size: 12px !important; + font-weight: 600 !important; + opacity: 0.6; + margin-top: 0 !important; + } + + .empty-conditions-container, + .add-condition-container { + grid-column: 1 / -1 !important; + } + + .filter-delete-button { + grid-column: 1 !important; + margin: 0 !important; + align-self: start !important; + } + + .quick-edit-toggle { + grid-column: 1 / -1 !important; + justify-self: flex-end; + margin: 0 0 12px !important; + align-self: start !important; + } + + .filter-line ::ng-deep .mat-mdc-form-field-subscript-wrapper { + display: none; + } + + .conditions-vertical-line { + display: none !important; + } +} + .settings-form__reset-button { margin-right: auto; } diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index 61e176ecb..dbeca3181 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -1,6 +1,9 @@

- New fast filter for {{ data.displayTableName }} + + {{ data.filtersSet.id ? 'Edit fast filter' : 'New fast filter' }} + {{ data.displayTableName }} +

Use conditions to quickly filter {{ data.displayTableName.toLowerCase() }} and optionally edit one column. @@ -49,170 +52,180 @@

subdirectory_arrow_right where

-
- Quick edit - - info_outline - - -
-

Enable to change this value
directly from the table view

-
-
-
-
-
+ +
+

Enable to change this value
directly from the table view

+
+
+
+
-
-
- status = active -
-
- status: - - - | - -
+
+
+
+ status = active +
+
+ status: + + + | +
- -
+
+
-
- - -
- {{value.key}} +
+
+ {{value.key}} + +
-
- -
+
+ + + + - - - -
+ + + + - - {{value.key}} + + + + + {{ {startswith: 'a…', endswith: '…a', eq: '=', contains: '∋', icontains: '∌', empty: '∅'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + + + play_arrow + starts with + + + play_arrow + ends with + + + drag_handle + equal + + + search + contains + + + block + not contains + + + space_bar + is empty + + + - - - - play_arrow - starts with - - - play_arrow - ends with - - - drag_handle - equal - - - search - contains - - - block - not contains - - - space_bar - is empty - - - + + + + {{ {eq: '=', gt: '>', lt: '<', gte: '≥', lte: '≤'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + + + drag_handle + equal + + + keyboard_arrow_right + greater than + + + keyboard_arrow_left + less than + + + keyboard_double_arrow_right + greater than or equal + + + keyboard_double_arrow_left + less than or equal + + + - - - - drag_handle - equal - - - keyboard_arrow_right - greater than - - - keyboard_arrow_left - less than - - - keyboard_double_arrow_right - greater than or equal - - - keyboard_double_arrow_left - less than or equal - - - +
+ +
+
+
- - - - - + +
diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index 185da6328..87e09fed1 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -301,12 +301,12 @@ flex-direction: row; flex-wrap: wrap; gap: 6px; - margin: 8px 8px 0; - padding: 5px 12px; + margin: 8px 0 16px; + padding: 8px 16px; border-radius: 10px; background: rgba(0, 0, 0, 0.03); transform: none; - max-width: calc(100% - 16px); + max-width: 100%; min-width: 0; overflow-x: clip; overflow-y: visible; @@ -332,17 +332,22 @@ } .dynamic-column-editor .column-name { - flex: 0 0 auto; - font-size: 13px; + flex: 1 1 100%; + font-size: 12px; line-height: 1.3; gap: 6px; + margin: 0; + opacity: 0.6; + font-weight: 600; white-space: nowrap; } .dynamic-column-editor .column-name strong { margin-left: 0; - color: rgba(0, 0, 0, 0.87); - font-size: 14px; + font-size: 12px; + font-weight: 600; + color: inherit; + text-transform: none; } .dynamic-column-editor .comparator-text { diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts index 878ebb9c3..6facb3ea8 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.ts @@ -254,6 +254,7 @@ export class SavedFiltersPanelComponent implements OnInit, OnDestroy { handleOpenSavedFiltersDialog(filtersSet: any = null) { const _dialogRef = this.dialog.open(SavedFiltersDialogComponent, { width: '56em', + panelClass: 'mobile-bottom-sheet-dialog', data: { connectionID: this.connectionID, tableName: this.selectedTableName, diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index da3988724..a02b6f221 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -125,8 +125,10 @@ body { border-radius: 16px 16px 0 0 !important; box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18) !important; max-height: 85vh; - overflow-y: auto; + overflow: hidden; padding-top: 16px !important; + display: flex; + flex-direction: column; } .mobile-bottom-sheet-dialog .filters-header { From 13d6f392d1b1272e1b4a08a1eefa349a79fbabeb Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Wed, 27 May 2026 13:10:05 +0300 Subject: [PATCH 05/11] feat(mobile): table picker, sort, and columns as bottom sheets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Table switcher: replaced the mat-menu (whose search overlapped the trigger and caused misclicks) with a bottom sheet — search pill, collapsible folders with colored folder icons (dehaze for All tables), active table checkmark, slide-up + scrim. Rendered at the component root with backdrop-filter/translateZ so the AI-config banner's backdrop-filter no longer composites over it. - Sort by: bottom sheet listing sortable columns; per-column asc/desc icon buttons (active highlighted) plus a lock toggle for set/remove default — replaces the awkward nested submenu. - Columns: opens the same drag-drop checkbox list as a bottom sheet on mobile (isMobileView nulls the matMenuTriggerFor) while desktop keeps the dropdown menu. - Comparator selects in filter dialogs show full text on desktop and a single symbol on mobile; table-name shown as "Table: " in both filter dialog headers. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../db-table-filters-dialog.component.html | 2 +- .../db-table-view/db-table-view.component.css | 352 +++++++++++++++++- .../db-table-view.component.html | 173 +++++++-- .../db-table-view/db-table-view.component.ts | 97 +++++ .../saved-filters-dialog.component.css | 88 +++-- .../saved-filters-dialog.component.html | 16 +- 6 files changed, 649 insertions(+), 79 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html index 175e618f7..7bc903df4 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html @@ -2,7 +2,7 @@

Filters - {{ data.displayTableName }} + Table: {{ data.displayTableName }}

{{ displayName }}

- - - -
{{ folder.category_name }}
- -
- -
Others
- -
-
- - - - - -
@@ -463,3 +437,140 @@

{{ displayName }}

+ + +
+
+ +
+ Select table + +
+ + + +
+ + + + + + + +
Others
+ +
+ +
+ No tables match "{{ tableSwitcherSearch }}". +
+
+
+ + +
+
+ +
+ Sort by + +
+ +
+
+ {{ tableData.dataNormalizedColumns[column] }} +
+ + + +
+
+
+
+ + +
+
+ +
+ Columns + +
+ +
+
+
+ drag_indicator + + {{ column.normalizedTitle }} + +
+
+
+
diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 9212f7d09..d71529e5b 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -311,6 +311,8 @@ export class DbTableViewComponent implements OnInit, OnChanges { return this.tableFolders?.find((cat) => cat.category_id === null)?.tables || []; } + public tableSwitcherSearch: string = ''; + get tableFoldersForSelect(): TableCategory[] { if (!this.tableFolders) return []; return this.tableFolders.filter((cat) => cat.category_id !== null); @@ -324,6 +326,89 @@ export class DbTableViewComponent implements OnInit, OnChanges { return this.allTables.filter((t) => !tablesInFolders.has(t.table)); } + get filteredTableFoldersForSelect(): TableCategory[] { + const query = this.tableSwitcherSearch.trim().toLowerCase(); + if (!query) return this.tableFoldersForSelect; + return this.tableFoldersForSelect + .map((folder) => ({ + ...folder, + tables: (folder.tables || []).filter((t) => this._matchesTableQuery(t, query)), + })) + .filter((folder) => folder.tables.length > 0); + } + + get filteredUncategorizedTables(): TableProperties[] { + const query = this.tableSwitcherSearch.trim().toLowerCase(); + if (!query) return this.uncategorizedTables; + return this.uncategorizedTables.filter((t) => this._matchesTableQuery(t, query)); + } + + @ViewChild('tableSwitcherSearchInput') + tableSwitcherSearchInputRef?: { nativeElement: HTMLInputElement }; + + public tableSwitcherOpen = false; + public collapsedFolders = new Set(); + public sortSheetOpen = false; + public columnsSheetOpen = false; + + get isMobileView(): boolean { + return typeof window !== 'undefined' && window.innerWidth <= 600; + } + + openSortSheet() { + this.sortSheetOpen = true; + } + + closeSortSheet() { + this.sortSheetOpen = false; + } + + openColumnsSheet() { + this.columnsSheetOpen = true; + } + + closeColumnsSheet() { + this.columnsSheetOpen = false; + } + + get sortableDataColumns(): string[] { + return (this.tableData?.displayedDataColumns || []).filter((c: string) => this.isSortable(c)); + } + + clearTableSwitcherSearch() { + this.tableSwitcherSearch = ''; + } + + toggleFolderCollapse(folderId: string) { + if (this.collapsedFolders.has(folderId)) { + this.collapsedFolders.delete(folderId); + } else { + this.collapsedFolders.add(folderId); + } + } + + isFolderCollapsed(folderId: string): boolean { + if (this.tableSwitcherSearch.trim()) return false; + return this.collapsedFolders.has(folderId); + } + + openTableSwitcher() { + this.tableSwitcherOpen = true; + setTimeout(() => { + this.tableSwitcherSearchInputRef?.nativeElement.focus(); + }, 50); + } + + closeTableSwitcher() { + this.tableSwitcherOpen = false; + this.clearTableSwitcherSearch(); + } + + switchTableFromSheet(tableName: string) { + this.closeTableSwitcher(); + this.switchTable(tableName); + } + loadRowsPage() { this.tableRelatedRecords = null; this.tableData.fetchRows({ @@ -926,4 +1011,16 @@ export class DbTableViewComponent implements OnInit, OnChanges { return rid ? this._permissions.canI(action, 'Table', rid)() : null; }); } + + private _matchesTableQuery(table: TableProperties, query: string): boolean { + const haystack = [ + (table as any).normalizedTableName, + table.display_name, + table.table, + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + return haystack.includes(query); + } } diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css index bf6c48441..c371fed70 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css @@ -732,51 +732,83 @@ } .condition-card__inputs > .comparator-select-field { - flex: 0 0 56px !important; + flex: 0 0 auto; +} + +.comparator-trigger__symbol { + display: none; +} + +.comparator-trigger__text { + display: inline; +} + +@media (width <= 600px) { + .condition-card__inputs > .comparator-select-field { + flex: 0 0 56px !important; + } + + .comparator-trigger__symbol { + display: inline; + } + + .comparator-trigger__text { + display: none; + } } .condition-card__value { - display: block; - width: 100%; + display: flex; min-width: 0; + flex: 1 1 0; } +.condition-card__value > *, .condition-card__value ::ng-deep .mat-mdc-form-field { - width: 100% !important; - display: block !important; + flex: 1 1 auto; + width: 100%; + min-width: 0; } .condition-card__value ::ng-deep .full-width { - width: 100% !important; + width: 100%; } .condition-card__inputs .comparator-select-field { - flex: 0 0 56px !important; - width: 56px !important; - min-width: 0 !important; - max-width: 56px !important; + flex: 0 0 auto; } -.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-text-field-wrapper { - min-width: 0 !important; - width: 56px !important; - padding-left: 8px !important; - padding-right: 4px !important; -} +@media (width <= 600px) { + .condition-card__inputs .comparator-select-field { + flex: 0 0 56px !important; + width: 56px !important; + max-width: 56px !important; + min-width: 0 !important; + } -.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-flex { - min-width: 0 !important; + .condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-text-field-wrapper { + width: 56px !important; + min-width: 0 !important; + padding-left: 8px !important; + padding-right: 4px !important; + } } -.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-infix { - min-width: 0 !important; - width: auto !important; - padding-left: 0 !important; - padding-right: 0 !important; -} +@media (width <= 600px) { + .condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-flex { + min-width: 0 !important; + } -.condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-select-arrow-wrapper { - padding-left: 0; + .condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-infix { + min-width: 0 !important; + width: auto !important; + padding-left: 0 !important; + padding-right: 0 !important; + } + + .condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-select-arrow-wrapper { + padding-left: 0; + } } .condition-card__inputs .comparator-select-field ::ng-deep .mat-mdc-form-field-subscript-wrapper { @@ -968,6 +1000,10 @@ pointer-events: none; } +.comparator-select-field ::ng-deep .mat-mdc-select-value { + padding-right: 24px; +} + .comparator-select-field ::ng-deep .mat-mdc-select-panel { min-width: max-content !important; width: max-content !important; diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html index dbeca3181..da9f2ddc2 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.html @@ -2,7 +2,7 @@

{{ data.filtersSet.id ? 'Edit fast filter' : 'New fast filter' }} - {{ data.displayTableName }} + Table: {{ data.displayTableName }}

@@ -131,7 +131,12 @@

[(ngModel)]="tableRowFieldsComparator[value.key]" (ngModelChange)="updateComparator($event, value.key)"> - {{ {startswith: 'a…', endswith: '…a', eq: '=', contains: '∋', icontains: '∌', empty: '∅'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + + {{ {startswith: 'a…', endswith: '…a', eq: '=', contains: '∋', icontains: '∌', empty: '∅'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + + + {{ {startswith: 'starts with', endswith: 'ends with', eq: 'equal', contains: 'contains', icontains: 'not contains', empty: 'is empty'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + play_arrow @@ -167,7 +172,12 @@

[(ngModel)]="tableRowFieldsComparator[value.key]" (ngModelChange)="updateComparator($event, value.key)"> - {{ {eq: '=', gt: '>', lt: '<', gte: '≥', lte: '≤'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + + {{ {eq: '=', gt: '>', lt: '<', gte: '≥', lte: '≤'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + + + {{ {eq: 'equal', gt: 'greater than', lt: 'less than', gte: 'greater than or equal', lte: 'less than or equal'}[tableRowFieldsComparator[value.key]] || tableRowFieldsComparator[value.key] }} + drag_handle From 5c083b16c368d0930af000b0d7a7cb1224625281 Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Wed, 27 May 2026 16:05:44 +0300 Subject: [PATCH 06/11] feat(mobile): pull-to-refresh, import/export sheet, sticky row-edit actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Table view: removed the refresh icon on mobile in favor of pull-to- refresh (touch handlers trigger loadRowsPage past a 70px pull with a spinner indicator); Import/Export opens as a bottom sheet on mobile while desktop keeps the dropdown menu. - Row preview: wider 20px side padding, header trailing icons aligned, copy-link snackbar shown at the top so it isn't hidden under the sheet. - Cell copy buttons hidden on mobile — they were invisible (no hover) but still tappable, causing accidental copies instead of opening the row preview. - Row edit form: action bar (Back / Save / Save and continue editing) fixed to the viewport bottom on mobile so it no longer scrolls off the page; the form scroll container is mat-drawer-content and position: fixed correctly pins (verified — no transformed ancestor), so a flex static footer is NOT used. Buttons reflow: Back+Save in a row, the long continue button on its own line; safe-area padding at the bottom. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../db-table-row-view.component.css | 11 ++-- .../db-table-row-view.component.ts | 2 +- .../db-table-view/db-table-view.component.css | 47 ++++++++++++++++ .../db-table-view.component.html | 45 ++++++++++++++-- .../db-table-view/db-table-view.component.ts | 54 +++++++++++++++++++ .../db-table-row-edit.component.css | 51 ++++++++++++++++++ .../base-table-display-field.component.css | 6 +++ .../src/app/services/notifications.service.ts | 3 +- frontend/src/styles.scss | 8 +++ 9 files changed, 217 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css index 050a413e7..a4cc7de50 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.css @@ -108,8 +108,8 @@ justify-content: space-between; padding-top: 20px; padding-bottom: 20px; - padding-left: 16px; - padding-right: 16px; + padding-left: 20px; + padding-right: 12px; z-index: 1; } @@ -129,7 +129,7 @@ flex-direction: column; align-items: flex-start; gap: 4px; - padding: 12px 16px; + padding: 12px 20px; } .row-preview-sidebar__field:not(:last-child) { @@ -148,12 +148,13 @@ } .related-records-section { - margin: 8px 4px 0; + margin: 8px 0 0; + padding: 0 20px; } .related-records-section__title { margin: 0 0 4px !important; - padding: 0 12px; + padding: 0; font-size: 12px !important; font-weight: 600 !important; line-height: 1.4 !important; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts index 50a96305c..13997e992 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts @@ -313,7 +313,7 @@ export class DbTableRowViewComponent implements OnInit, OnDestroy { } showCopyNotification(message: string) { - this._notifications.showSuccessSnackbar(message); + this._notifications.showSuccessSnackbar(message, 'top'); } stashUrlParams() { diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css index de2309f67..92efeab42 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css @@ -2,6 +2,43 @@ display: none; } +.pull-refresh-indicator { + display: none; +} + +@media (width <= 600px) { + .pull-refresh-indicator { + display: flex; + align-items: center; + justify-content: center; + height: 0; + overflow: hidden; + transition: height 150ms ease; + } + + .pull-refresh-indicator__icon { + opacity: 0.6; + transition: transform 100ms linear; + } + + .pull-refresh-indicator__icon_spinning { + animation: pullRefreshSpin 700ms linear infinite; + } + + .refresh-button { + display: none !important; + } +} + +@keyframes pullRefreshSpin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + .db-table-header { display: flex; justify-content: flex-end; @@ -257,6 +294,16 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + margin-right: auto; +} + +.table-sheet__item-icon { + font-size: 22px; + width: 22px; + height: 22px; + flex: 0 0 auto; + margin-right: 12px; + opacity: 0.7; } .table-sheet__item-check { diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 4bd9e0136..0e3faf81d 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -1,3 +1,13 @@ +
+ + refresh + +
+

{{ displayName }}

@@ -7,7 +17,7 @@

{{ displayName }}

arrow_drop_down -
@@ -129,10 +139,10 @@

{{ displayName }}

@@ -574,3 +584,32 @@

{{ displayName }}

+ + +
+
+ +
+ Import / Export + +
+ +
+ + +
+
diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index d71529e5b..16c9c32c4 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -7,6 +7,7 @@ import { Component, computed, EventEmitter, + HostListener, Input, inject, OnChanges, @@ -371,6 +372,16 @@ export class DbTableViewComponent implements OnInit, OnChanges { this.columnsSheetOpen = false; } + public transferSheetOpen = false; + + openTransferSheet() { + this.transferSheetOpen = true; + } + + closeTransferSheet() { + this.transferSheetOpen = false; + } + get sortableDataColumns(): string[] { return (this.tableData?.displayedDataColumns || []).filter((c: string) => this.isSortable(c)); } @@ -424,6 +435,49 @@ export class DbTableViewComponent implements OnInit, OnChanges { }); } + public pullDistance = 0; + public pullRefreshing = false; + private _pullStartY = 0; + private _pullTracking = false; + private readonly _pullThreshold = 70; + + @HostListener('touchstart', ['$event']) + onPullTouchStart(event: TouchEvent) { + if (!this.isMobileView || this.pullRefreshing) return; + const scrollTop = window.scrollY || document.documentElement.scrollTop || 0; + if (scrollTop > 0) return; + this._pullStartY = event.touches[0].clientY; + this._pullTracking = true; + } + + @HostListener('touchmove', ['$event']) + onPullTouchMove(event: TouchEvent) { + if (!this._pullTracking || this.pullRefreshing) return; + const delta = event.touches[0].clientY - this._pullStartY; + if (delta <= 0) { + this.pullDistance = 0; + return; + } + this.pullDistance = Math.min(delta * 0.5, this._pullThreshold + 20); + } + + @HostListener('touchend') + onPullTouchEnd() { + if (!this._pullTracking) return; + this._pullTracking = false; + if (this.pullDistance >= this._pullThreshold) { + this.pullRefreshing = true; + this.pullDistance = this._pullThreshold; + this.loadRowsPage(); + setTimeout(() => { + this.pullRefreshing = false; + this.pullDistance = 0; + }, 800); + } else { + this.pullDistance = 0; + } + } + isSortable(column: string) { return this.tableData.sortByColumns.includes(column) || !this.tableData.sortByColumns.length; } diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css index 163666bf7..84c191da4 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css @@ -206,6 +206,57 @@ } } +@media (width <= 600px) { + .wrapper { + padding-bottom: 132px; + } + + .actions { + position: fixed; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: auto; + min-height: 0; + margin: 0; + padding: 8px 16px max(8px, env(safe-area-inset-bottom, 0px)); + box-sizing: border-box; + border-top: 1px solid rgba(0, 0, 0, 0.08); + z-index: 100; + } + + .wrapper_shifted .actions { + left: 0; + } + + .actions-box { + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 8px; + width: 100%; + margin-left: 0; + } + + .actions-box > * { + flex: 1 1 0; + min-width: 0; + } + + .actions__continue { + order: 3; + flex: 1 1 100%; + margin: 0 !important; + } +} + +@media (prefers-color-scheme: dark) and (width <= 600px) { + .actions { + border-top-color: rgba(255, 255, 255, 0.1); + } +} + .actions__continue { margin-left: auto; margin-right: 20px; diff --git a/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.css b/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.css index 42ce1b697..99c057261 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.css +++ b/frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.css @@ -24,3 +24,9 @@ .field-display:hover .field-copy-button { opacity: 1; } + +@media (width <= 600px) { + .field-copy-button { + display: none !important; + } +} diff --git a/frontend/src/app/services/notifications.service.ts b/frontend/src/app/services/notifications.service.ts index 8925e670f..b04d1fa82 100644 --- a/frontend/src/app/services/notifications.service.ts +++ b/frontend/src/app/services/notifications.service.ts @@ -18,10 +18,11 @@ export class NotificationsService { }); } - showSuccessSnackbar(message: string) { + showSuccessSnackbar(message: string, verticalPosition: 'top' | 'bottom' = 'bottom') { this.snackBar.open(message, null, { duration: 2500, horizontalPosition: 'left', + verticalPosition, }); } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index a02b6f221..ee99bb913 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -232,3 +232,11 @@ body { } } + +@media (width <= 600px) { + /* Hide cell copy buttons on mobile — invisible (no hover) but still + tappable, causing accidental copies instead of opening row preview. */ + app-db-table-view .field-copy-button { + display: none !important; + } +} From d58cda951a4b197ad16a4240ba756040d65cceee Mon Sep 17 00:00:00 2001 From: Karina Kharchenko Date: Mon, 1 Jun 2026 11:58:59 +0300 Subject: [PATCH 07/11] feat(mobile): rework table header, action bar, sort sheet, and row edit form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Table view header on mobile: a 12px gap separates the table-switcher row from the search; the AI icon and Settings gear moved to the table-switcher row (far right) so the actions row stays uncluttered. - Filter actions split out of the icon-only actions row: Filter is a small outlined button under a full-width search field; Add row turned into an extended FAB (icon + label) pinned to the bottom-right, shown whenever the user has add permission (no longer hidden on empty tables) so it never disappears behind the empty-state CTA. - Columns moved next to Sort by in the saved-filters row, with the saved-filters-row switched from absolute Sort to a flex layout so Columns + Sort sit side-by-side at the right; both are icon-only on mobile (Columns view_week, Sort by gets a leading swap_vert icon). The actions-row originals are hidden on mobile. - Sort bottom sheet redesigned: tapping a column expands inline buttons for Ascending / Descending and a Set/Remove default action with a pin icon; an active sort arrow is appended to the column data-label in row cards. - Import/Export consolidated into the Settings menu on mobile (with a divider) so the separate swap_vert button can be hidden. - Filter dialog: autoFocus disabled so the "Add filter by..." input doesn't auto-open the column dropdown; settings icon explicitly pushed to the right via margin-left:auto. - Row edit form: breadcrumbs replaced with a back arrow and a two-line title (page mode + table name); page top margin reduced and empty app-alert collapsed so the title sits higher; widget info icon next to fields removed. Action bar reworked — Back removed entirely (the header arrow handles it); Save & continue pushed left, primary submit (Save / Add / Edit / Duplicate) right; on mobile the bar is a fixed bottom bar with safe-area padding and the buttons reflow with Save & continue on its own full-width row. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dashboard/dashboard.component.ts | 1 + .../db-table-filters-dialog.component.css | 5 + .../db-table-filters-dialog.component.html | 1 + .../db-table-view/db-table-view.component.css | 223 +++++++++++++++--- .../db-table-view.component.html | 219 +++++++++++------ .../db-table-view/db-table-view.component.ts | 6 + .../db-table-row-edit.component.css | 66 ++++-- .../db-table-row-edit.component.html | 29 ++- .../db-table-row-edit.component.ts | 7 + 9 files changed, 422 insertions(+), 135 deletions(-) diff --git a/frontend/src/app/components/dashboard/dashboard.component.ts b/frontend/src/app/components/dashboard/dashboard.component.ts index 9970174c5..52bf8caaf 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.ts +++ b/frontend/src/app/components/dashboard/dashboard.component.ts @@ -289,6 +289,7 @@ export class DashboardComponent implements OnInit, OnDestroy { let filterDialodRef = this.dialog.open(DbTableFiltersDialogComponent, { width: '56em', panelClass: 'mobile-bottom-sheet-dialog', + autoFocus: false, data: { connectionID: this.connectionID, tableName: this.selectedTableName, diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.css index 02f88dbdd..ba76b0eab 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.css @@ -6,6 +6,11 @@ margin-bottom: 12px; } +.filters-header__settings { + flex: 0 0 auto; + margin-left: auto; +} + .filters-header__text { display: flex; flex-direction: column; diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html index 7bc903df4..ba75065cb 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html @@ -5,6 +5,7 @@

Table: {{ data.displayTableName }} settings diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css index 92efeab42..fc6e05603 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.css @@ -66,6 +66,7 @@ .db-table-title { flex-grow: 1; margin-right: 0; + margin-bottom: 12px; justify-content: space-between; } } @@ -81,6 +82,40 @@ } } +.table-title__settings, +.table-title__ai { + display: none; +} + +.settings-menu__transfer { + display: none !important; +} + +@media (width <= 600px) { + .table-title__ai { + display: inline-flex; + margin-left: auto; + } + + .table-title__settings { + display: inline-flex; + } + + .actions__settings, + .actions__transfer, + .ai-insights-button { + display: none !important; + } + + button.settings-menu__transfer { + display: flex !important; + } + + mat-divider.settings-menu__transfer { + display: block !important; + } +} + .table-switcher { display: none; } @@ -326,8 +361,19 @@ align-items: center; justify-content: space-between; gap: 8px; - padding: 6px 12px; + width: 100%; + padding: 12px; + border: none; + background: transparent; border-radius: 8px; + cursor: pointer; + text-align: left; + color: inherit; + font: inherit; +} + +.sort-sheet__row_expanded { + background: rgba(0, 0, 0, 0.04); } .sort-sheet__col-name { @@ -339,51 +385,81 @@ font-size: 15px; } -.sort-sheet__controls { +.sort-sheet__row-indicator { + flex: 0 0 auto; + font-size: 20px; + width: 20px; + height: 20px; + opacity: 0.5; +} + +.sort-sheet__options { display: flex; - align-items: center; + flex-direction: column; gap: 2px; - flex: 0 0 auto; + padding: 2px 8px 10px 8px; } -.sort-sheet__dir-btn, -.sort-sheet__default-btn { - --mat-icon-button-state-layer-size: 36px; - width: 36px !important; - height: 36px !important; - opacity: 0.55; +.sort-sheet__opt { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + border: none; + background: transparent; + text-align: left; + padding: 10px 12px; + border-radius: 8px; + font-size: 14px; + color: inherit; + cursor: pointer; + opacity: 0.85; } -.sort-sheet__dir-btn .mat-icon, -.sort-sheet__default-btn .mat-icon { +.sort-sheet__opt .mat-icon { font-size: 20px; width: 20px; height: 20px; + flex: 0 0 auto; + opacity: 0.7; } -.sort-sheet__dir-btn_active { - opacity: 1; - color: var(--color-accentedPalette-600); - background: var(--color-accentedPalette-50); - border-radius: 50%; +.sort-sheet__opt:active { + background: rgba(0, 0, 0, 0.06); +} + +.sort-sheet__opt:disabled { + opacity: 0.35; + cursor: default; } -.sort-sheet__default-btn_active { +.sort-sheet__opt_active { opacity: 1; - color: var(--color-accentedPalette-600); + font-weight: 600; + color: var(--color-accentedPalette-700); + background: var(--color-accentedPalette-50); } -.sort-sheet__default-btn { - margin-left: 6px; +.sort-sheet__opt_active .mat-icon { + opacity: 1; + color: var(--color-accentedPalette-700); } @media (prefers-color-scheme: dark) { - .sort-sheet__dir-btn_active { + .sort-sheet__row_expanded { + background: rgba(255, 255, 255, 0.06); + } + + .sort-sheet__opt:active { + background: rgba(255, 255, 255, 0.08); + } + + .sort-sheet__opt_active { background: var(--color-accentedPalette-900); - color: var(--color-accentedPalette-200); + color: var(--color-accentedPalette-100); } - .sort-sheet__default-btn_active { + .sort-sheet__opt_active .mat-icon { color: var(--color-accentedPalette-200); } } @@ -529,24 +605,87 @@ opacity: 0.6; } +.search-row { + display: contents; +} + +.search-row__buttons { + display: contents; +} + +.search-row__filter { + display: none; +} + +.add-row-fab { + display: none; +} + @media (width <= 600px) { - .search-form { + .add-row-fab { display: inline-flex; + position: fixed; + right: 16px; + bottom: calc(16px + env(safe-area-inset-bottom, 0px)); + z-index: 50; + } +} + +.saved-filters-row__columns { + display: none; +} + +@media (width <= 600px) { + .search-row { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + width: 100%; + } + + .search-form { + display: flex; align-items: center; - flex: 0 0 auto; - max-width: 40px; + width: 100%; height: 40px; border-radius: 999px; background: rgba(0, 0, 0, 0.06); overflow: hidden; - transition: max-width 280ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 200ms; } - .search-form:focus-within, - .search-form_filled { - flex: 1 1 100%; - max-width: 100%; - background: rgba(0, 0, 0, 0.04); + .search-row__buttons { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + } + + .search-row__filter { + display: inline-flex; + flex: 0 0 auto; + height: 32px; + line-height: 32px; + padding: 0 12px; + border-radius: 999px; + font-size: 13px; + } + + .search-row__filter .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } + + .saved-filters-row__columns { + display: inline-flex; + flex: 0 0 auto; + } + + .actions__filter, + .actions__add-row, + .db-table-manage-columns-button { + display: none !important; } .search-input { @@ -622,11 +761,14 @@ @media (width <= 600px) { .saved-filters-row { - display: block; - position: relative; + display: flex; + align-items: flex-start; + gap: 8px; } .saved-filters-row > app-saved-filters-panel { + flex: 1 1 auto; + min-width: 0; display: block; } @@ -634,13 +776,18 @@ display: inline-flex !important; align-items: center; gap: 4px; + flex: 0 0 auto; min-height: 36px; padding: 0 10px; font-size: 13px; font-weight: 500; - position: absolute; - top: 0; - right: 0; + } + + .saved-filters-row__columns { + display: inline-flex !important; + align-items: center; + justify-content: center; + flex: 0 0 auto; } .mobile-sort-button__label { diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html index 0e3faf81d..af7131eb3 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html @@ -20,6 +20,26 @@

{{ displayName }}

+ + + +

@@ -49,24 +69,37 @@

{{ displayName }}

- - - search - - + + +
+ - - +
+
@@ -160,6 +195,7 @@

{{ displayName }}

- - - UI Widgets - - - Automations -
- - Primary keys are required. - -
- - Settings - -
+ + + + UI Widgets + + + Automations +
+ + Primary keys are required. + +
+ + Settings + + + + + +
+ + add + Add row + +
{{ displayName }} (filterSelected)="onFilterSelected($event)" > + +
@@ -308,7 +386,9 @@

{{ displayName }}

- +
{{ displayName }}
-
- {{ tableData.dataNormalizedColumns[column] }} -
- + +
+ - -
-
+
diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index 16c9c32c4..359a875e2 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -350,6 +350,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { public tableSwitcherOpen = false; public collapsedFolders = new Set(); public sortSheetOpen = false; + public sortExpandedColumn: string | null = null; public columnsSheetOpen = false; get isMobileView(): boolean { @@ -362,6 +363,11 @@ export class DbTableViewComponent implements OnInit, OnChanges { closeSortSheet() { this.sortSheetOpen = false; + this.sortExpandedColumn = null; + } + + toggleSortColumn(column: string) { + this.sortExpandedColumn = this.sortExpandedColumn === column ? null : column; } openColumnsSheet() { diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css index 84c191da4..100d0bb99 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.css @@ -2,10 +2,20 @@ --alert-margin: 24px; } +:host app-alert:empty { + display: none; +} + .page { display: flex; height: 100%; - margin: 2.5em 0 16px; + margin: 16px 0; +} + +@media (width <= 600px) { + .page { + margin: 8px 0 0; + } } .wrapper { @@ -30,14 +40,44 @@ .row-edit-header { display: flex; align-items: center; + justify-content: space-between; gap: 16px; } +.row-edit-header__main { + display: flex; + align-items: center; + gap: 4px; + min-width: 0; +} + +.row-edit-header__back { + flex: 0 0 auto; +} + +.row-edit-header__text { + display: flex; + flex-direction: column; + line-height: 1.2; + min-width: 0; +} + +.row-edit-header__title { + font-size: 18px; + font-weight: 600; +} + +.row-edit-header__subtitle { + font-size: 12px; + font-weight: 500; + opacity: 0.6; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + @media (width <= 600px) { .row-edit-header { - flex-direction: column; - align-items: flex-start; - gap: 8px; width: 100%; } } @@ -180,7 +220,8 @@ .actions-box { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-end; + gap: 8px; width: 100%; } @@ -234,20 +275,15 @@ flex-direction: row; flex-wrap: wrap; align-items: center; + justify-content: flex-end; gap: 8px; width: 100%; margin-left: 0; } - .actions-box > * { - flex: 1 1 0; - min-width: 0; - } - .actions__continue { - order: 3; - flex: 1 1 100%; - margin: 0 !important; + margin-right: auto !important; + margin-left: 0 !important; } } @@ -258,8 +294,8 @@ } .actions__continue { - margin-left: auto; - margin-right: 20px; + margin-right: auto; + margin-left: 0; } .error-details { diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html index 52589d951..2507bf608 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html @@ -20,7 +20,19 @@
- +
+ + arrow_back + +
+ {{ pageTitle }} + {{ dispalyTableName }} +
+
@@ -166,12 +171,6 @@

- - Back - -
+
+ + + +
+ {{ displayName }}

[tableTypes]="tableData.tableTypes" (filterSelected)="onFilterSelected($event)" > - - - -
diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css index c371fed70..0bae5269f 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.css @@ -735,6 +735,20 @@ flex: 0 0 auto; } +/* ndc-dynamic renders the component as a sibling and leaves an empty anchor element behind; keep it from taking up flex space */ +.condition-card__inputs > ndc-dynamic { + flex: 0 0 auto; + width: auto; +} + +/* the actual component is rendered as the sibling right after the anchor; let it (and its host child) fill the available space */ +.condition-card__inputs > ndc-dynamic + *, +.condition-card__inputs ::ng-deep ndc-dynamic + * { + flex: 1 1 0; + width: 100%; + min-width: 0; +} + .comparator-trigger__symbol { display: none; } @@ -770,6 +784,20 @@ min-width: 0; } +/* ndc-dynamic renders the component as a sibling and leaves an empty anchor element behind; keep it from taking up flex space */ +.condition-card__value > ndc-dynamic { + flex: 0 0 auto; + width: auto; +} + +/* the actual component is rendered as the sibling right after the anchor; let it (and its host child) fill the available space */ +.condition-card__value > ndc-dynamic + *, +.condition-card__value ::ng-deep ndc-dynamic + * { + flex: 1 1 0; + width: 100%; + min-width: 0; +} + .condition-card__value ::ng-deep .full-width { width: 100%; } diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css index 87e09fed1..2180b4c10 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.css @@ -30,6 +30,25 @@ display: inline-flex; } + .saved-filters-trigger_active { + width: calc(100vw - 130px - 2*24px); + box-sizing: border-box; + } + + /* let the button's label shrink and ellipsis when it can't fit into calc(100% - 130px) */ + .saved-filters-trigger_active ::ng-deep .mdc-button__label { + display: flex; + align-items: center; + min-width: 0; + overflow: hidden; + } + + .saved-filters-trigger_active .saved-filters-trigger__label { + max-width: none; + min-width: 0; + flex: 1 1 auto; + } + .create-filter-button, .saved-filters-tabs { display: none !important; @@ -301,7 +320,8 @@ flex-direction: row; flex-wrap: wrap; gap: 6px; - margin: 8px 0 16px; + margin-top: 0; + margin-bottom: 16px; padding: 8px 16px; border-radius: 10px; background: rgba(0, 0, 0, 0.03); From abadad6e014c8ee45675a49871c54292c0283838 Mon Sep 17 00:00:00 2001 From: Lyubov Voloshko Date: Tue, 2 Jun 2026 19:02:14 +0000 Subject: [PATCH 11/11] fix notifications service unit test for verticalPosition Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/app/services/notifications.service.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/services/notifications.service.spec.ts b/frontend/src/app/services/notifications.service.spec.ts index 3fbcd423f..e24be5c6d 100644 --- a/frontend/src/app/services/notifications.service.spec.ts +++ b/frontend/src/app/services/notifications.service.spec.ts @@ -55,6 +55,7 @@ describe('NotificationsService', () => { Object({ duration: 2500, horizontalPosition: 'left', + verticalPosition: 'bottom', }), ); });