mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 22:13:32 +00:00
Merge branch 'main' into PM-29919-Add-dropdown-to-select-email-verification-and-emails-field-to-Send-when-creating-or-editing-a-Send
This commit is contained in:
46
.github/workflows/repository-management.yml
vendored
46
.github/workflows/repository-management.yml
vendored
@@ -72,7 +72,6 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Validate version input format
|
||||
@@ -111,8 +110,7 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||
permission-contents: write # for creating, committing to, and pushing new branches
|
||||
permission-pull-requests: write # for generating pull requests
|
||||
permission-contents: write # for committing and pushing to main (bypasses rulesets)
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
@@ -448,53 +446,15 @@ jobs:
|
||||
echo "No changes to commit!";
|
||||
fi
|
||||
|
||||
- name: Create version bump branch
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: |
|
||||
BRANCH_NAME="version-bump-$(date +%s)"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Commit version bumps with GPG signature
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: |
|
||||
git commit -m "Bumped client version(s)" -a
|
||||
|
||||
- name: Push version bump branch
|
||||
- name: Push changes to main
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: |
|
||||
git push --set-upstream origin "$BRANCH_NAME"
|
||||
|
||||
- name: Create Pull Request for version bump
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
VERSION_BROWSER: ${{ steps.set-final-version-output.outputs.version_browser }}
|
||||
VERSION_CLI: ${{ steps.set-final-version-output.outputs.version_cli }}
|
||||
VERSION_DESKTOP: ${{ steps.set-final-version-output.outputs.version_desktop }}
|
||||
VERSION_WEB: ${{ steps.set-final-version-output.outputs.version_web }}
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token }}
|
||||
script: |
|
||||
const versions = [];
|
||||
if (process.env.VERSION_BROWSER) versions.push(`- Browser: ${process.env.VERSION_BROWSER}`);
|
||||
if (process.env.VERSION_CLI) versions.push(`- CLI: ${process.env.VERSION_CLI}`);
|
||||
if (process.env.VERSION_DESKTOP) versions.push(`- Desktop: ${process.env.VERSION_DESKTOP}`);
|
||||
if (process.env.VERSION_WEB) versions.push(`- Web: ${process.env.VERSION_WEB}`);
|
||||
|
||||
const body = versions.length > 0
|
||||
? `Automated version bump:\n\n${versions.join('\n')}`
|
||||
: 'Automated version bump';
|
||||
|
||||
const { data: pr } = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Bumped client version(s)',
|
||||
body: body,
|
||||
head: process.env.BRANCH_NAME,
|
||||
base: context.ref.replace('refs/heads/', '')
|
||||
});
|
||||
console.log(`Created PR #${pr.number}: ${pr.html_url}`);
|
||||
git push
|
||||
|
||||
cut_branch:
|
||||
name: Cut branch
|
||||
|
||||
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@@ -111,7 +111,7 @@ jobs:
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo build
|
||||
|
||||
- name: Test Ubuntu
|
||||
- name: Linux unit tests
|
||||
if: ${{ matrix.os=='ubuntu-22.04' }}
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: |
|
||||
@@ -120,17 +120,21 @@ jobs:
|
||||
mkdir -p ~/.local/share/keyrings
|
||||
eval "$(printf '\n' | gnome-keyring-daemon --unlock)"
|
||||
eval "$(printf '\n' | /usr/bin/gnome-keyring-daemon --start)"
|
||||
cargo test -- --test-threads=1
|
||||
cargo test --lib -- --test-threads=1
|
||||
|
||||
- name: Test macOS
|
||||
- name: MacOS unit tests
|
||||
if: ${{ matrix.os=='macos-14' }}
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo test -- --test-threads=1
|
||||
run: cargo test --lib -- --test-threads=1
|
||||
|
||||
- name: Test Windows
|
||||
- name: Windows unit tests
|
||||
if: ${{ matrix.os=='windows-2022'}}
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo test --workspace --exclude=desktop_napi -- --test-threads=1
|
||||
run: cargo test --lib --workspace --exclude=desktop_napi -- --test-threads=1
|
||||
|
||||
- name: Doc tests
|
||||
working-directory: ./apps/desktop/desktop_native
|
||||
run: cargo test --doc
|
||||
|
||||
rust-coverage:
|
||||
name: Rust Coverage
|
||||
|
||||
@@ -96,7 +96,9 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
*/
|
||||
async getPageDetails(): Promise<AutofillPageDetails> {
|
||||
// Set up listeners on top-layer candidates that predate Mutation Observer setup
|
||||
this.setupInitialTopLayerListeners();
|
||||
if (this.autofillOverlayContentService) {
|
||||
this.setupInitialTopLayerListeners();
|
||||
}
|
||||
|
||||
if (!this.mutationObserver) {
|
||||
this.setupMutationObserver();
|
||||
@@ -1072,19 +1074,21 @@ export class CollectAutofillContentService implements CollectAutofillContentServ
|
||||
}
|
||||
|
||||
private setupTopLayerCandidateListener = (element: Element) => {
|
||||
const ownedTags = this.autofillOverlayContentService.getOwnedInlineMenuTagNames() || [];
|
||||
this.ownedExperienceTagNames = ownedTags;
|
||||
if (this.autofillOverlayContentService) {
|
||||
const ownedTags = this.autofillOverlayContentService.getOwnedInlineMenuTagNames() || [];
|
||||
this.ownedExperienceTagNames = ownedTags;
|
||||
|
||||
if (!ownedTags.includes(element.tagName)) {
|
||||
element.addEventListener("toggle", (event: ToggleEvent) => {
|
||||
if (event.newState === "open") {
|
||||
// Add a slight delay (but faster than a user's reaction), to ensure the layer
|
||||
// positioning happens after any triggered toggle has completed.
|
||||
setTimeout(this.autofillOverlayContentService.refreshMenuLayerPosition, 100);
|
||||
}
|
||||
});
|
||||
if (!ownedTags.includes(element.tagName)) {
|
||||
element.addEventListener("toggle", (event: ToggleEvent) => {
|
||||
if (event.newState === "open") {
|
||||
// Add a slight delay (but faster than a user's reaction), to ensure the layer
|
||||
// positioning happens after any triggered toggle has completed.
|
||||
setTimeout(this.autofillOverlayContentService.refreshMenuLayerPosition, 100);
|
||||
}
|
||||
});
|
||||
|
||||
this.autofillOverlayContentService.refreshMenuLayerPosition();
|
||||
this.autofillOverlayContentService.refreshMenuLayerPosition();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class PopupSizeService {
|
||||
/** Begin listening for state changes */
|
||||
async init() {
|
||||
this.width$.subscribe((width: PopupWidthOption) => {
|
||||
PopupSizeService.setStyle(width);
|
||||
void PopupSizeService.setStyle(width);
|
||||
localStorage.setItem(PopupSizeService.LocalStorageKey, width);
|
||||
});
|
||||
}
|
||||
@@ -77,8 +77,9 @@ export class PopupSizeService {
|
||||
}
|
||||
}
|
||||
|
||||
private static setStyle(width: PopupWidthOption) {
|
||||
if (!BrowserPopupUtils.inPopup(window)) {
|
||||
private static async setStyle(width: PopupWidthOption) {
|
||||
const isInTab = await BrowserPopupUtils.isInTab();
|
||||
if (!BrowserPopupUtils.inPopup(window) || isInTab) {
|
||||
return;
|
||||
}
|
||||
const pxWidth = PopupWidthOptions[width] ?? PopupWidthOptions.default;
|
||||
@@ -91,6 +92,6 @@ export class PopupSizeService {
|
||||
**/
|
||||
static initBodyWidthFromLocalStorage() {
|
||||
const storedValue = localStorage.getItem(PopupSizeService.LocalStorageKey);
|
||||
this.setStyle(storedValue as any);
|
||||
void this.setStyle(storedValue as any);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,5 +19,14 @@ windows-core = { workspace = true }
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_Graphics_Gdi",
|
||||
] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
324
apps/desktop/desktop_native/autotype/tests/integration_tests.rs
Normal file
324
apps/desktop/desktop_native/autotype/tests/integration_tests.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use autotype::{get_foreground_window_title, type_input};
|
||||
use serial_test::serial;
|
||||
use tracing::debug;
|
||||
use windows::Win32::{
|
||||
Foundation::{COLORREF, HINSTANCE, HMODULE, HWND, LPARAM, LRESULT, WPARAM},
|
||||
Graphics::Gdi::{CreateSolidBrush, UpdateWindow, ValidateRect, COLOR_WINDOW},
|
||||
System::LibraryLoader::{GetModuleHandleA, GetModuleHandleW},
|
||||
UI::WindowsAndMessaging::*,
|
||||
};
|
||||
use windows_core::{s, w, Result, PCSTR, PCWSTR};
|
||||
|
||||
struct TestWindow {
|
||||
handle: HWND,
|
||||
capture: Option<InputCapture>,
|
||||
}
|
||||
|
||||
impl Drop for TestWindow {
|
||||
fn drop(&mut self) {
|
||||
// Clean up the InputCapture pointer
|
||||
unsafe {
|
||||
let capture_ptr = GetWindowLongPtrW(self.handle, GWLP_USERDATA) as *mut InputCapture;
|
||||
if !capture_ptr.is_null() {
|
||||
let _ = Box::from_raw(capture_ptr);
|
||||
}
|
||||
CloseWindow(self.handle).expect("window handle should be closeable");
|
||||
DestroyWindow(self.handle).expect("window handle should be destroyable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// state to capture keyboard input
|
||||
#[derive(Clone)]
|
||||
struct InputCapture {
|
||||
chars: Arc<Mutex<Vec<char>>>,
|
||||
}
|
||||
|
||||
impl InputCapture {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
chars: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_chars(&self) -> Vec<char> {
|
||||
self.chars
|
||||
.lock()
|
||||
.expect("mutex should not be poisoned")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom window procedure that captures input
|
||||
unsafe extern "system" fn capture_input_proc(
|
||||
handle: HWND,
|
||||
msg: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
match msg {
|
||||
WM_CREATE => {
|
||||
// Store the InputCapture pointer in window data
|
||||
let create_struct = lparam.0 as *const CREATESTRUCTW;
|
||||
let capture_ptr = (*create_struct).lpCreateParams as *mut InputCapture;
|
||||
SetWindowLongPtrW(handle, GWLP_USERDATA, capture_ptr as isize);
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_CHAR => {
|
||||
// Get the InputCapture from window data
|
||||
let capture_ptr = GetWindowLongPtrW(handle, GWLP_USERDATA) as *mut InputCapture;
|
||||
if !capture_ptr.is_null() {
|
||||
let capture = &*capture_ptr;
|
||||
if let Some(ch) = char::from_u32(wparam.0 as u32) {
|
||||
capture
|
||||
.chars
|
||||
.lock()
|
||||
.expect("mutex should not be poisoned")
|
||||
.push(ch);
|
||||
}
|
||||
}
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcW(handle, msg, wparam, lparam),
|
||||
}
|
||||
}
|
||||
|
||||
// A pointer to the window procedure
|
||||
type ProcType = unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT;
|
||||
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc>
|
||||
extern "system" fn show_window_proc(
|
||||
handle: HWND, // the window handle
|
||||
message: u32, // the system message
|
||||
wparam: WPARAM, /* additional message information. The contents of the wParam parameter
|
||||
* depend on the value of the message parameter. */
|
||||
lparam: LPARAM, /* additional message information. The contents of the lParam parameter
|
||||
* depend on the value of the message parameter. */
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match message {
|
||||
WM_PAINT => {
|
||||
debug!("WM_PAINT");
|
||||
let res = ValidateRect(Some(handle), None);
|
||||
debug_assert!(res.ok().is_ok());
|
||||
LRESULT(0)
|
||||
}
|
||||
WM_DESTROY => {
|
||||
debug!("WM_DESTROY");
|
||||
PostQuitMessage(0);
|
||||
LRESULT(0)
|
||||
}
|
||||
_ => DefWindowProcA(handle, message, wparam, lparam),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestWindow {
|
||||
fn set_foreground(&self) -> Result<()> {
|
||||
unsafe {
|
||||
let _ = ShowWindow(self.handle, SW_SHOW);
|
||||
let _ = SetForegroundWindow(self.handle);
|
||||
let _ = UpdateWindow(self.handle);
|
||||
let _ = SetForegroundWindow(self.handle);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_for_input(&self, timeout_ms: u64) {
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed().as_millis() < timeout_ms as u128 {
|
||||
process_messages();
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_messages() {
|
||||
unsafe {
|
||||
let mut msg = MSG::default();
|
||||
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
|
||||
let _ = TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_input_window(title: PCWSTR, proc_type: ProcType) -> Result<TestWindow> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleW(None).unwrap_or(HMODULE(std::ptr::null_mut()));
|
||||
let instance: HINSTANCE = instance.into();
|
||||
debug_assert!(!instance.is_invalid());
|
||||
|
||||
let window_class = w!("show_window");
|
||||
|
||||
// Register window class with our custom proc
|
||||
let wc = WNDCLASSW {
|
||||
lpfnWndProc: Some(proc_type),
|
||||
hInstance: instance,
|
||||
lpszClassName: window_class,
|
||||
hbrBackground: CreateSolidBrush(COLORREF(
|
||||
(COLOR_WINDOW.0 + 1).try_into().expect("i32 to fit in u32"),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _atom = RegisterClassW(&wc);
|
||||
|
||||
let capture = InputCapture::new();
|
||||
|
||||
// Pass InputCapture as lpParam
|
||||
let capture_ptr = Box::into_raw(Box::new(capture.clone()));
|
||||
|
||||
// Create window
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window>
|
||||
let handle = CreateWindowExW(
|
||||
WINDOW_EX_STYLE(0),
|
||||
window_class,
|
||||
title,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
400,
|
||||
300,
|
||||
None,
|
||||
None,
|
||||
Some(instance),
|
||||
Some(capture_ptr as *const _),
|
||||
)
|
||||
.expect("window should be created");
|
||||
|
||||
// Process pending messages
|
||||
process_messages();
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
Ok(TestWindow {
|
||||
handle,
|
||||
capture: Some(capture),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn create_title_window(title: PCSTR, proc_type: ProcType) -> Result<TestWindow> {
|
||||
unsafe {
|
||||
let instance = GetModuleHandleA(None)?;
|
||||
let instance: HINSTANCE = instance.into();
|
||||
debug_assert!(!instance.is_invalid());
|
||||
|
||||
let window_class = s!("input_window");
|
||||
|
||||
// Register window class with our custom proc
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassa>
|
||||
let wc = WNDCLASSA {
|
||||
hCursor: LoadCursorW(None, IDC_ARROW)?,
|
||||
hInstance: instance,
|
||||
lpszClassName: window_class,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(proc_type),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _atom = RegisterClassA(&wc);
|
||||
|
||||
// Create window
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window>
|
||||
let handle = CreateWindowExA(
|
||||
WINDOW_EX_STYLE::default(),
|
||||
window_class,
|
||||
title,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
800,
|
||||
600,
|
||||
None,
|
||||
None,
|
||||
Some(instance),
|
||||
None,
|
||||
)
|
||||
.expect("window should be created");
|
||||
|
||||
Ok(TestWindow {
|
||||
handle,
|
||||
capture: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn test_get_active_window_title_success() {
|
||||
let title;
|
||||
{
|
||||
let window = create_title_window(s!("TITLE_FOOBAR"), show_window_proc).unwrap();
|
||||
window.set_foreground().unwrap();
|
||||
title = get_foreground_window_title().unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(title, "TITLE_FOOBAR\0".to_owned());
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn test_get_active_window_title_doesnt_fail_if_empty_title() {
|
||||
let title;
|
||||
{
|
||||
let window = create_title_window(s!(""), show_window_proc).unwrap();
|
||||
window.set_foreground().unwrap();
|
||||
title = get_foreground_window_title();
|
||||
}
|
||||
|
||||
assert_eq!(title.unwrap(), "".to_owned());
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[test]
|
||||
fn test_type_input_success() {
|
||||
const TAB: u16 = 0x09;
|
||||
let chars;
|
||||
{
|
||||
let window = create_input_window(w!("foo"), capture_input_proc).unwrap();
|
||||
window.set_foreground().unwrap();
|
||||
|
||||
type_input(
|
||||
&[
|
||||
0x66, 0x6F, 0x6C, 0x6C, 0x6F, 0x77, 0x5F, 0x74, 0x68, 0x65, TAB, 0x77, 0x68, 0x69,
|
||||
0x74, 0x65, 0x5F, 0x72, 0x61, 0x62, 0x62, 0x69, 0x74,
|
||||
],
|
||||
&["Control".to_owned(), "Alt".to_owned(), "B".to_owned()],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Wait for and process input messages
|
||||
window.wait_for_input(250);
|
||||
|
||||
// Verify captured input
|
||||
let capture = window.capture.as_ref().unwrap();
|
||||
chars = capture.get_chars();
|
||||
}
|
||||
|
||||
assert!(!chars.is_empty(), "No input captured");
|
||||
|
||||
let input_str = String::from_iter(chars.iter());
|
||||
let input_str = input_str.replace("\t", "_");
|
||||
|
||||
assert_eq!(input_str, "follow_the_white_rabbit");
|
||||
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
@@ -12,9 +12,13 @@ if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ]; then
|
||||
export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libdbus-1.so.3"
|
||||
fi
|
||||
|
||||
# A bug in Electron 39 (which now enables Wayland by default) causes a crash on
|
||||
# systems using Wayland with hardware acceleration. Platform decided to
|
||||
# configure Electron to use X11 (with an opt-out) until the upstream bug is
|
||||
# fixed. The follow-up task is https://bitwarden.atlassian.net/browse/PM-31080.
|
||||
PARAMS="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto"
|
||||
if [ "$USE_X11" = "true" ]; then
|
||||
PARAMS=""
|
||||
if [ "$USE_X11" != "false" ]; then
|
||||
PARAMS="--ozone-platform=x11"
|
||||
fi
|
||||
|
||||
$APP_PATH/bitwarden-app $PARAMS "$@"
|
||||
|
||||
@@ -32,8 +32,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ApproveSshRequestComponent } from "../../platform/components/approve-ssh-request";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { ApproveSshRequestComponent } from "../components/approve-ssh-request";
|
||||
import { SshAgentPromptType } from "../models/ssh-agent-setting";
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
[text]="organization.node.name"
|
||||
[appA11yTitle]="organization.node.name"
|
||||
(click)="applyFilter($event, organization)"
|
||||
/>
|
||||
@if (!organization.node.enabled) {
|
||||
<span class="tw-ml-auto">
|
||||
>
|
||||
@if (!organization.node.enabled) {
|
||||
<i
|
||||
slot="end"
|
||||
class="bwi bwi-fw bwi-exclamation-triangle text-danger mr-auto"
|
||||
[attr.aria-label]="'organizationIsDisabled' | i18n"
|
||||
[appA11yTitle]="'organizationIsDisabled' | i18n"
|
||||
></i>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</bit-nav-item>
|
||||
}
|
||||
</bit-nav-group>
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ import {
|
||||
VaultFilter,
|
||||
VaultFilterServiceAbstraction as VaultFilterService,
|
||||
RoutedVaultFilterBridgeService,
|
||||
VaultItemsTransferService,
|
||||
DefaultVaultItemsTransferService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
@@ -130,6 +132,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
provide: COPY_CLICK_LISTENER,
|
||||
useExisting: VaultComponent,
|
||||
},
|
||||
{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService },
|
||||
],
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
@@ -214,6 +217,7 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService,
|
||||
private routedVaultFilterBridgeService: RoutedVaultFilterBridgeService,
|
||||
private vaultFilterService: VaultFilterService,
|
||||
private vaultItemTransferService: VaultItemsTransferService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -266,6 +270,11 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
if (this.vaultItemsComponent) {
|
||||
await this.vaultItemsComponent.refresh().catch(() => {});
|
||||
}
|
||||
if (this.activeUserId) {
|
||||
void this.vaultItemTransferService.enforceOrganizationDataOwnership(
|
||||
this.activeUserId,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "modalShown":
|
||||
this.showingModal = true;
|
||||
@@ -372,6 +381,8 @@ export class VaultComponent implements OnInit, OnDestroy, CopyClickListener {
|
||||
.subscribe((collections) => {
|
||||
this.filteredCollections = collections;
|
||||
});
|
||||
|
||||
void this.vaultItemTransferService.enforceOrganizationDataOwnership(this.activeUserId);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
@@ -92,6 +92,8 @@ import {
|
||||
PasswordRepromptService,
|
||||
CipherFormComponent,
|
||||
ArchiveCipherUtilitiesService,
|
||||
VaultItemsTransferService,
|
||||
DefaultVaultItemsTransferService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { NavComponent } from "../../../app/layout/nav.component";
|
||||
@@ -150,6 +152,7 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
provide: COPY_CLICK_LISTENER,
|
||||
useExisting: VaultV2Component,
|
||||
},
|
||||
{ provide: VaultItemsTransferService, useClass: DefaultVaultItemsTransferService },
|
||||
],
|
||||
})
|
||||
export class VaultV2Component<C extends CipherViewLike>
|
||||
@@ -264,6 +267,7 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
private policyService: PolicyService,
|
||||
private archiveCipherUtilitiesService: ArchiveCipherUtilitiesService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
private vaultItemTransferService: VaultItemsTransferService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -317,6 +321,11 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
.catch(() => {});
|
||||
await this.vaultFilterComponent.reloadOrganizations().catch(() => {});
|
||||
}
|
||||
if (this.activeUserId) {
|
||||
void this.vaultItemTransferService.enforceOrganizationDataOwnership(
|
||||
this.activeUserId,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "modalShown":
|
||||
this.showingModal = true;
|
||||
@@ -420,6 +429,8 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
.subscribe((collections) => {
|
||||
this.allCollections = collections;
|
||||
});
|
||||
|
||||
void this.vaultItemTransferService.enforceOrganizationDataOwnership(this.activeUserId);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3,11 +3,11 @@ import { svgIcon } from "../icon-service";
|
||||
const BitwardenShield = svgIcon`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 32" fill="none">
|
||||
<g clip-path="url(#bitwarden-shield-clip)">
|
||||
<path class="tw-fill-text-alt2" d="M22.01 17.055V4.135h-9.063v22.954c1.605-.848 3.041-1.77 4.31-2.766 3.169-2.476 4.753-4.899 4.753-7.268Zm3.884-15.504v15.504a9.256 9.256 0 0 1-.677 3.442 12.828 12.828 0 0 1-1.68 3.029 18.708 18.708 0 0 1-2.386 2.574 27.808 27.808 0 0 1-2.56 2.08 32.251 32.251 0 0 1-2.448 1.564c-.85.49-1.453.824-1.81.999-.357.175-.644.31-.86.404-.162.08-.337.12-.526.12s-.364-.04-.526-.12a22.99 22.99 0 0 1-.86-.404c-.357-.175-.96-.508-1.81-1a32.242 32.242 0 0 1-2.448-1.564 27.796 27.796 0 0 1-2.56-2.08 18.706 18.706 0 0 1-2.386-2.573 12.828 12.828 0 0 1-1.68-3.029A9.256 9.256 0 0 1 0 17.055V1.551C0 1.2.128.898.384.642.641.386.944.26 1.294.26H24.6c.35 0 .654.127.91.383s.384.559.384.909Z"/>
|
||||
<path class="tw-fill-fg-sidenav-text" d="M22.01 17.055V4.135h-9.063v22.954c1.605-.848 3.041-1.77 4.31-2.766 3.169-2.476 4.753-4.899 4.753-7.268Zm3.884-15.504v15.504a9.256 9.256 0 0 1-.677 3.442 12.828 12.828 0 0 1-1.68 3.029 18.708 18.708 0 0 1-2.386 2.574 27.808 27.808 0 0 1-2.56 2.08 32.251 32.251 0 0 1-2.448 1.564c-.85.49-1.453.824-1.81.999-.357.175-.644.31-.86.404-.162.08-.337.12-.526.12s-.364-.04-.526-.12a22.99 22.99 0 0 1-.86-.404c-.357-.175-.96-.508-1.81-1a32.242 32.242 0 0 1-2.448-1.564 27.796 27.796 0 0 1-2.56-2.08 18.706 18.706 0 0 1-2.386-2.573 12.828 12.828 0 0 1-1.68-3.029A9.256 9.256 0 0 1 0 17.055V1.551C0 1.2.128.898.384.642.641.386.944.26 1.294.26H24.6c.35 0 .654.127.91.383s.384.559.384.909Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="bitwarden-shield-clip">
|
||||
<path class="tw-fill-text-alt2" d="M0 0h26v32H0z"/>
|
||||
<path class="tw-fill-fg-sidenav-text" d="M0 0h26v32H0z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@@ -71,9 +71,9 @@ const styles: Record<IconButtonType, string[]> = {
|
||||
primary: ["!tw-text-primary-600", "focus-visible:before:tw-ring-primary-600", ...focusRing],
|
||||
danger: ["!tw-text-danger-600", "focus-visible:before:tw-ring-primary-600", ...focusRing],
|
||||
"nav-contrast": [
|
||||
"!tw-text-alt2",
|
||||
"!tw-text-fg-sidenav-text",
|
||||
"hover:!tw-bg-hover-contrast",
|
||||
"focus-visible:before:tw-ring-text-alt2",
|
||||
"focus-visible:before:tw-ring-border-focus",
|
||||
...focusRing,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<ng-template #button>
|
||||
<button
|
||||
type="button"
|
||||
class="tw-ms-auto"
|
||||
class="tw-ms-auto tw-text-fg-sidenav-text"
|
||||
[ngClass]="{
|
||||
'tw-transform tw-rotate-[90deg]': variantValue === 'tree' && !open(),
|
||||
}"
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
<div
|
||||
[style.padding-inline-start]="navItemIndentationPadding()"
|
||||
class="tw-relative tw-rounded-md tw-h-10"
|
||||
[class.tw-bg-background-alt4]="showActiveStyles"
|
||||
[class.tw-bg-background-alt3]="!showActiveStyles"
|
||||
[class.hover:tw-bg-hover-contrast]="!showActiveStyles"
|
||||
[class.tw-bg-bg-sidenav-active-item]="showActiveStyles"
|
||||
[class.tw-bg-bg-sidenav-background]="!showActiveStyles"
|
||||
[class.hover:tw-bg-bg-sidenav-item-hover]="!showActiveStyles"
|
||||
[class]="fvwStyles$ | async"
|
||||
>
|
||||
<div class="tw-relative tw-flex tw-items-center tw-h-full">
|
||||
@if (open) {
|
||||
<div
|
||||
class="tw-absolute tw-left-[0px] tw-transform tw--translate-x-[calc(100%_+_4px)] [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2"
|
||||
class="tw-absolute tw-left-[0px] tw-transform tw--translate-x-[calc(100%_+_4px)] [&>*:focus-visible::before]:!tw-ring-border-focus [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2"
|
||||
>
|
||||
<ng-content select="[slot=start]"></ng-content>
|
||||
</div>
|
||||
@@ -31,7 +31,7 @@
|
||||
>
|
||||
@if (icon()) {
|
||||
<i
|
||||
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-alt2 {{ icon() }}"
|
||||
class="!tw-m-0 tw-w-4 tw-shrink-0 bwi bwi-fw tw-text-fg-sidenav-text {{ icon() }}"
|
||||
[attr.aria-hidden]="open"
|
||||
[attr.aria-label]="text()"
|
||||
></i>
|
||||
@@ -47,7 +47,7 @@
|
||||
<!-- The `data-fvw` attribute passes focus to `this.focusVisibleWithin$` -->
|
||||
<!-- The following `class` field should match the `#isButton` class field below -->
|
||||
<a
|
||||
class="tw-size-full tw-px-4 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
|
||||
class="tw-size-full tw-px-4 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-fg-sidenav-text hover:tw-text-fg-sidenav-text hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
|
||||
[class.!tw-ps-0]="variant() === 'tree' || treeDepth() > 0"
|
||||
data-fvw
|
||||
[routerLink]="route()"
|
||||
@@ -68,7 +68,7 @@
|
||||
<!-- Class field should match `#isAnchor` class field above -->
|
||||
<button
|
||||
type="button"
|
||||
class="tw-size-full tw-px-4 tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-alt2 hover:tw-text-alt2 hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
|
||||
class="tw-size-full tw-px-4 tw-block tw-truncate tw-border-none tw-bg-transparent tw-text-start !tw-text-fg-sidenav-text hover:tw-text-fg-sidenav-text hover:tw-no-underline focus:tw-outline-none [&_i]:tw-leading-[1.5rem]"
|
||||
[class.!tw-ps-0]="variant() === 'tree' || treeDepth() > 0"
|
||||
data-fvw
|
||||
(click)="mainContentClicked.emit()"
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
@if (open) {
|
||||
<div
|
||||
class="tw-flex tw-items-center tw-pe-1 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-text-alt2 [&>*:hover]:!tw-border-text-alt2 [&>*]:tw-text-alt2 empty:tw-hidden"
|
||||
class="tw-flex tw-items-center tw-pe-1 tw-gap-1 [&>*:focus-visible::before]:!tw-ring-border-focus [&>*:hover]:!tw-border-border-focus [&>*]:tw-text-fg-sidenav-text empty:tw-hidden"
|
||||
>
|
||||
<ng-content select="[slot=end]"></ng-content>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@ export class NavItemComponent extends NavBaseComponent {
|
||||
protected focusVisibleWithin$ = new BehaviorSubject(false);
|
||||
protected fvwStyles$ = this.focusVisibleWithin$.pipe(
|
||||
map((value) =>
|
||||
value ? "tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-inset tw-ring-text-alt2" : "",
|
||||
value ? "tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-inset tw-ring-border-focus" : "",
|
||||
),
|
||||
);
|
||||
@HostListener("focusin", ["$event.target"])
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<!-- absolutely position the link svg to avoid shifting layout when sidenav is closed -->
|
||||
<a
|
||||
[routerLink]="route()"
|
||||
class="tw-relative tw-p-3 tw-block tw-rounded-md tw-bg-background-alt3 tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-text-alt2 hover:tw-bg-hover-contrast tw-h-[73px] [&_svg]:tw-absolute [&_svg]:tw-inset-[.6875rem] [&_svg]:tw-w-[200px]"
|
||||
class="tw-relative tw-p-3 tw-block tw-rounded-md tw-bg-bg-sidenav tw-outline-none focus-visible:tw-ring focus-visible:tw-ring-inset focus-visible:tw-ring-border-focus hover:tw-bg-bg-sidenav-item-hover tw-h-[73px] [&_svg]:tw-absolute [&_svg]:tw-inset-[.6875rem] [&_svg]:tw-w-[200px]"
|
||||
[ngClass]="{
|
||||
'!tw-h-[55px] [&_svg]:!tw-w-[26px]': !sideNavService.open,
|
||||
}"
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
<div class="tw-relative tw-h-full">
|
||||
<nav
|
||||
id="bit-side-nav"
|
||||
class="tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-full tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-background-alt3 tw-outline-none"
|
||||
class="tw-sticky tw-inset-y-0 tw-left-0 tw-z-30 tw-flex tw-h-full tw-flex-col tw-overscroll-none tw-overflow-auto tw-bg-bg-sidenav tw-text-fg-sidenav-text tw-outline-none"
|
||||
[style.width.rem]="data.open ? (sideNavService.width$ | async) : undefined"
|
||||
[ngStyle]="
|
||||
variant() === 'secondary' && {
|
||||
'--color-text-alt2': 'var(--color-text-main)',
|
||||
'--color-background-alt3': 'var(--color-secondary-100)',
|
||||
'--color-background-alt4': 'var(--color-secondary-300)',
|
||||
'--color-hover-contrast': 'var(--color-hover-default)',
|
||||
'--color-sidenav-text': 'var(--color-admin-sidenav-text)',
|
||||
'--color-sidenav-background': 'var(--color-admin-sidenav-background)',
|
||||
'--color-sidenav-active-item': 'var(--color-admin-sidenav-active-item)',
|
||||
'--color-sidenav-item-hover': 'var(--color-admin-sidenav-item-hover)',
|
||||
}
|
||||
"
|
||||
[cdkTrapFocus]="data.isOverlay"
|
||||
@@ -27,7 +27,7 @@
|
||||
<!-- 53rem = ~850px -->
|
||||
<!-- This is a magic number. This number was selected by going to the UI and finding the number that felt the best to me and design. No real rhyme or reason :) -->
|
||||
<div
|
||||
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full tw-bg-background-alt3"
|
||||
class="[@media(min-height:53rem)]:tw-sticky tw-bottom-0 tw-left-0 tw-z-20 tw-mt-auto tw-w-full"
|
||||
>
|
||||
<bit-nav-divider></bit-nav-divider>
|
||||
@if (data.open) {
|
||||
|
||||
@@ -353,6 +353,19 @@
|
||||
|
||||
/* Focus Border */
|
||||
--color-border-focus: var(--color-black);
|
||||
|
||||
/**========================================
|
||||
* SIDENAV BACKGROUND COLORS (Light mode)
|
||||
* ======================================== */
|
||||
--color-sidenav-background: var(--color-brand-800);
|
||||
--color-sidenav-text: var(--color-white);
|
||||
--color-sidenav-active-item: var(--color-brand-900);
|
||||
--color-sidenav-item-hover: var(--color-brand-900);
|
||||
|
||||
--color-admin-sidenav-background: var(--color-gray-100);
|
||||
--color-admin-sidenav-text: var(--color-gray-900);
|
||||
--color-admin-sidenav-active-item: var(--color-gray-300);
|
||||
--color-admin-sidenav-item-hover: var(--color-gray-300);
|
||||
}
|
||||
|
||||
.theme_light {
|
||||
@@ -542,6 +555,19 @@
|
||||
|
||||
/* Focus Border */
|
||||
--color-border-focus: var(--color-white);
|
||||
|
||||
/**========================================
|
||||
* SIDENAV BACKGROUND COLORS (Dark mode)
|
||||
* ======================================== */
|
||||
--color-sidenav-background: var(--color-gray-800);
|
||||
--color-sidenav-text: var(--color-white);
|
||||
--color-sidenav-active-item: var(--color-gray-900);
|
||||
--color-sidenav-item-hover: var(--color-gray-900);
|
||||
|
||||
--color-admin-sidenav-background: var(--color-gray-800);
|
||||
--color-admin-sidenav-text: var(--color-white);
|
||||
--color-admin-sidenav-active-item: var(--color-gray-900);
|
||||
--color-admin-sidenav-item-hover: var(--color-gray-900);
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
||||
@@ -72,11 +72,11 @@ module.exports = {
|
||||
code: rgba("--color-text-code"),
|
||||
},
|
||||
background: {
|
||||
DEFAULT: rgba("--color-background"),
|
||||
alt: rgba("--color-background-alt"),
|
||||
alt2: rgba("--color-background-alt2"),
|
||||
alt3: rgba("--color-background-alt3"),
|
||||
alt4: rgba("--color-background-alt4"),
|
||||
DEFAULT: "var(--color-bg-primary)",
|
||||
alt: "var(--color-bg-tertiary)",
|
||||
alt2: "var(--color-bg-brand)",
|
||||
alt3: "var(--color-bg-brand-strong)",
|
||||
alt4: "var(--color-brand-950)",
|
||||
},
|
||||
bg: {
|
||||
white: "var(--color-bg-white)",
|
||||
@@ -117,6 +117,9 @@ module.exports = {
|
||||
"accent-tertiary": "var(--color-bg-accent-tertiary)",
|
||||
hover: "var(--color-bg-hover)",
|
||||
overlay: "var(--color-bg-overlay)",
|
||||
sidenav: "var(--color-sidenav-background)",
|
||||
"sidenav-active-item": "var(--color-sidenav-active-item)",
|
||||
"sidenav-item-hover": "var(--color-sidenav-item-hover)",
|
||||
},
|
||||
hover: {
|
||||
default: "var(--color-hover-default)",
|
||||
@@ -159,6 +162,7 @@ module.exports = {
|
||||
"accent-tertiary-soft": "var(--color-fg-accent-tertiary-soft)",
|
||||
"accent-tertiary": "var(--color-fg-accent-tertiary)",
|
||||
"accent-tertiary-strong": "var(--color-fg-accent-tertiary-strong)",
|
||||
"sidenav-text": "var(--color-sidenav-text)",
|
||||
},
|
||||
border: {
|
||||
muted: "var(--color-border-muted)",
|
||||
@@ -253,6 +257,7 @@ module.exports = {
|
||||
"fg-accent-tertiary-soft": "var(--color-fg-accent-tertiary-soft)",
|
||||
"fg-accent-tertiary": "var(--color-fg-accent-tertiary)",
|
||||
"fg-accent-tertiary-strong": "var(--color-fg-accent-tertiary-strong)",
|
||||
"fg-sidenav-text": "var(--color-sidenav-text)",
|
||||
}),
|
||||
borderColor: ({ theme }) => ({
|
||||
...theme("colors"),
|
||||
|
||||
Reference in New Issue
Block a user