♻️ Use DS buttons in the dashboard team modals (#12537)

* ♻️ Use DS buttons in the dashboard team modals

The close buttons of the create/rename team modal, the team permission
notice and the two "before you leave" modals become ghost DS icon
buttons named "Close"; the class that places them goes on the tooltip
wrapper. The cancel and "promote and leave" inputs of the leave modals
become DS buttons: secondary, and destructive once a member is picked
(primary and disabled before).

Two fixtures make the leave and reassign modal testable (the logged-in
user owns the second team, which has another member). A visual test
covers the create team modal and the leave and reassign modal.

AI-assisted-by: claude-opus-5-5

* ♻️ Move dashboard team modal styles closer to the DS

The change owner and team form stylesheets resolve their legacy color
aliases, spacing and typography mixins to DS tokens and use the DS
typography module without a namespace. The legacy uppercase title
becomes the DS headline-small typography, which differs only in its
size (12px instead of 11px): the "before you leave" title grows 1px and
the modal 2px. Unused selectors are commented out with a TODO.

refactor/ stays only for the modal placeholders (%modal-*), which move
with the modals refactor, and the input placeholders.

AI-assisted-by: claude-opus-5-5

* 🔥 Remove unused styles from the dashboard team modals

The SCSS refactor of the dashboard team modals left rules commented out
with a TODO because the module does not use them, or because they target
an svg inside a DS icon that is the svg itself. CSS modules scope the
classes to this module and no other source uses them, so they go:
.input-wrapper, .cancel-button, &.danger. No visual change.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco 2026-10-27 10:58:13 +01:00 committed by GitHub
parent 19fc9ebce8
commit afd838ea5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 203 additions and 83 deletions

View File

@ -0,0 +1,30 @@
[
{
"~:is-admin": false,
"~:email": "bar@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Han Solo",
"~:fullname": "Han Solo",
"~:is-owner": false,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:profile-id": "~u1e162163-87b7-805b-8005-5fd05514b6d3",
"~:created-at": "~m1733324626956"
},
{
"~:is-admin": true,
"~:email": "foo@example.com",
"~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:name": "Princesa Leia",
"~:fullname": "Princesa Leia",
"~:is-owner": true,
"~:modified-at": "~m1713533116365",
"~:can-edit": true,
"~:is-active": true,
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b",
"~:created-at": "~m1713533116365"
}
]

View File

@ -0,0 +1,48 @@
[
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116375",
"~:is-default": true
},
{
"~:features": {
"~#set": [
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Second team",
"~:modified-at": "~m1701164272671",
"~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3",
"~:created-at": "~m1701164272671",
"~:is-default": false
}
]

View File

@ -438,3 +438,51 @@ test.describe("Subscription controls", () => {
await expect(page).toHaveScreenshot(shot);
});
});
test.describe("Team modals controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };
test("Create team modal", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.goToDashboard();
await dashboardPage.teamDropdown.click();
await page.getByText("Create new team").click();
const modal = page.locator("[class*='modal-container']");
await expect(modal).toBeVisible();
await page.mouse.move(10, 700);
await expect(modal).toHaveScreenshot(shot);
await modal.locator("[class*='modal-header'] button").hover();
await expect(modal).toHaveScreenshot(shot);
});
test("Leave and reassign modal", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
// The logged-in user owns the second team, which has another member.
await dashboardPage.mockRPC(
"get-teams",
"dashboard/get-teams-second-owner.json",
);
await dashboardPage.mockRPC(
"get-team-members?team-id=*",
"dashboard/get-team-members-owner.json",
);
await dashboardPage.goToSecondTeamMembersSection();
await expect(page.getByText("role")).toBeVisible();
const rows = page.locator("[class*='table-rows']");
await rows
.locator("[class*='menu-btn']")
.or(rows.getByRole("button", { name: "Options" }))
.first()
.click();
await rows.getByText("Leave team").click();
const modal = page.locator("[class*='modal-container']");
await expect(modal).toBeVisible();
await page.mouse.move(10, 700);
await expect(modal).toHaveScreenshot(shot);
});
});

View File

@ -12,7 +12,9 @@
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -45,8 +47,11 @@
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} (tr "modals.leave-and-reassign.title")]
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:> icon-button* {:variant "ghost"
:tooltip-class (stl/css :modal-close-btn)
:aria-label (tr "labels.close")
:icon i/close
:on-click modal/hide!}]]
[:div {:class (stl/css :modal-content)}
[:p {:class (stl/css :modal-msg)}
@ -62,19 +67,16 @@
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:input {:class (stl/css :cancel-button)
:type "button"
:value (tr "labels.cancel")
:on-click modal/hide!}]
[:> button* {:variant "secondary"
:type "button"
:on-click modal/hide!}
(tr "labels.cancel")]
[:input.accept-button
{:type "button"
:class (stl/css-case :accept-btn true
:danger (:valid @form)
:global/disabled (not (:valid @form)))
:disabled (not (:valid @form))
:value (tr "modals.leave-and-reassign.promote-and-leave")
:on-click on-accept}]]]]]))
[:> button* {:variant (if (:valid @form) "destructive" "primary")
:type "button"
:disabled (not (:valid @form))
:on-click on-accept}
(tr "modals.leave-and-reassign.promote-and-leave")]]]]]))
@ -149,8 +151,11 @@
[:div {:class (stl/css :modal-organization-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-organization-title)} (tr "modals.before-leave-organization.title")]
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:> icon-button* {:variant "ghost"
:tooltip-class (stl/css :modal-close-btn)
:aria-label (tr "labels.close")
:icon i/close
:on-click modal/hide!}]]
[:div {:class (stl/css :modal-content)}
(if (zero? num-teams-to-delete)
@ -169,16 +174,13 @@
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:input {:class (stl/css :cancel-button)
:type "button"
:value (tr "labels.cancel")
:on-click modal/hide!}]
[:> button* {:variant "secondary"
:type "button"
:on-click modal/hide!}
(tr "labels.cancel")]
[:input.accept-button
{:type "button"
:class (stl/css-case :accept-btn true
:danger all-valid?
:global/disabled (not all-valid?))
:disabled (not all-valid?)
:value (tr "modals.leave-and-reassign.promote-and-leave")
:on-click on-accept}]]]]]))
[:> button* {:variant (if all-valid? "destructive" "primary")
:type "button"
:disabled (not all-valid?)
:on-click on-accept}
(tr "modals.leave-and-reassign.promote-and-leave")]]]]]))

View File

@ -4,8 +4,10 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
// refactor/ stays for the modal placeholders (%modal-*, to move with the
// modals refactor) and the input placeholder (%input-with-label, with the inputs).
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/typography.scss" as t;
@use "ds/typography.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/z-index.scss" as *;
@ -20,48 +22,34 @@
}
.modal-header {
margin-bottom: deprecated.$s-24;
margin-block-end: var(--sp-xxl);
}
.modal-title {
@include deprecated.uppercase-title-typography;
@include use-typography("headline-small");
color: var(--modal-title-foreground-color);
color: var(--color-foreground-primary);
}
// Tooltip wrapper of the close button: it places it in the corner.
.modal-close-btn {
@extend %modal-close-btn-base;
position: absolute;
inset-block-start: var(--sp-s);
inset-inline-end: var(--sp-xs);
}
.modal-content {
@include deprecated.body-small-typography;
@include use-typography("body-small");
margin-bottom: deprecated.$s-24;
}
.input-wrapper {
@extend %input-with-label;
@include deprecated.body-small-typography;
margin-block-end: var(--sp-xxl);
}
.action-buttons {
@extend %modal-action-btns;
}
.cancel-button {
@extend %modal-cancel-btn;
}
.accept-btn {
@extend %modal-accept-btn;
&.danger {
@extend %modal-danger-btn;
}
}
.modal-msg {
color: var(--modal-text-foreground-color);
color: var(--color-foreground-secondary);
}
.teams-container {
@ -75,34 +63,34 @@
display: grid;
grid-template-columns: 1fr 2fr;
align-items: center;
width: 100%;
inline-size: 100%;
}
.modal-organization-container {
@extend %modal-container-base;
overflow-y: auto;
max-height: $sz-512;
max-block-size: $sz-512;
}
.modal-organization-title {
@include t.use-typography("headline-large");
@include use-typography("headline-large");
color: var(--modal-title-foreground-color);
color: var(--color-foreground-primary);
}
.modal-organization-msg {
@include t.use-typography("body-large");
@include use-typography("body-large");
color: var(--modal-text-foreground-color);
color: var(--color-foreground-secondary);
}
.team-name {
@include t.use-typography("body-medium");
@include use-typography("body-medium");
color: var(--modal-text-foreground-color);
color: var(--color-foreground-secondary);
}
.team-member {
@include t.use-typography("body-medium");
@include use-typography("body-medium");
}

View File

@ -18,7 +18,8 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
@ -118,8 +119,11 @@
[:h2 {:class (stl/css :modal-title)}
(tr "labels.create-team")])
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:> icon-button* {:variant "ghost"
:tooltip-class (stl/css :modal-close-btn)
:aria-label (tr "labels.close")
:icon i/close
:on-click modal/hide!}]]
[:div {:class (stl/css :modal-content)}
[:& fm/input {:type "text"
@ -162,7 +166,10 @@
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} title]
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:> icon-button* {:variant "ghost"
:tooltip-class (stl/css :modal-close-btn)
:aria-label (tr "labels.close")
:icon i/close
:on-click modal/hide!}]]
[:div {:class (stl/css :modal-content)}
[:div message]]]]))

View File

@ -4,9 +4,11 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
// refactor/ stays for the modal placeholders (%modal-*, to move with the
// modals refactor) and the input placeholder (%input-element-label, with the inputs).
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_utils.scss" as *;
@use "ds/typography.scss" as t;
@use "ds/typography.scss" as *;
.modal-overlay {
@extend %modal-overlay-base;
@ -21,20 +23,23 @@
}
.modal-title {
@include t.use-typography("headline-large");
@include use-typography("headline-large");
color: var(--modal-title-foreground-color);
color: var(--color-foreground-primary);
}
// Tooltip wrapper of the close button: it places it in the corner.
.modal-close-btn {
@extend %modal-close-btn-base;
position: absolute;
inset-block-start: var(--sp-s);
inset-inline-end: var(--sp-xs);
}
.modal-content {
margin-block-end: var(--sp-xxl);
color: var(--color-foreground-secondary);
@include t.use-typography("body-large");
@include use-typography("body-large");
}
.team-form {
@ -52,24 +57,16 @@
flex-direction: column;
gap: var(--sp-xs);
align-items: flex-start;
width: 100%;
inline-size: 100%;
border: none;
background-color: transparent;
height: 100%;
block-size: 100%;
}
.action-buttons {
@extend %modal-action-btns;
}
.cancel-button {
@extend %modal-cancel-btn;
}
.accept-btn {
@extend %modal-accept-btn;
&.danger {
@extend %modal-danger-btn;
}
}