diff --git a/.gitignore b/.gitignore index 89a9f8b902..3c0e926a9f 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,9 @@ client_secret.json /app/mailsync.exe /app/mailsync.dSYM .idea + +# Debug symbol files +**/*.pdb + +# Claude Code local settings +.claude/settings.local.json diff --git a/app/internal_packages/account-sidebar/lib/sidebar-section.ts b/app/internal_packages/account-sidebar/lib/sidebar-section.ts index 5d181ca74e..a46ddd9049 100644 --- a/app/internal_packages/account-sidebar/lib/sidebar-section.ts +++ b/app/internal_packages/account-sidebar/lib/sidebar-section.ts @@ -5,11 +5,14 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import _ from 'underscore'; +import React from 'react'; import { Actions, Account, SyncbackCategoryTask, CategoryStore, + AccountGroupStore, + AccountGroup, Label, ExtensionRegistry, RegExpUtils, @@ -83,6 +86,120 @@ class SidebarSection { }; } + static _nameStyleForGroup(group: AccountGroup): React.CSSProperties | undefined { + const style: React.CSSProperties = {}; + if (group.color) { + style.color = group.color; + } + const ds = group.displayStyle || 'normal'; + if (ds === 'bold' || ds === 'bold-italic') { + style.fontWeight = 700; + } + if (ds === 'italic' || ds === 'bold-italic') { + style.fontStyle = 'italic'; + } + if (Object.keys(style).length === 0) return undefined; + return style; + } + + static _buildGroupedCategoryChildren( + accounts: Account[], + names: string[], + groups: AccountGroup[] + ): ISidebarItem[] { + const children: ISidebarItem[] = []; + const groupedAccountIds = new Set(); + + for (const group of groups) { + const groupAccounts = accounts.filter(acc => group.accountIds.includes(acc.id)); + if (groupAccounts.length === 0) continue; + + groupAccounts.forEach(acc => groupedAccountIds.add(acc.id)); + + const groupCats = _.compact( + groupAccounts.map(acc => + _.first(_.compact(names.map(name => CategoryStore.getCategoryByRole(acc, name)))) + ) + ); + if (groupCats.length === 0) continue; + + const groupChildren = groupAccounts + .map(acc => { + const cat = _.first( + _.compact(names.map(name => CategoryStore.getCategoryByRole(acc, name))) + ); + if (!cat) return null; + return SidebarItem.forCategories([cat], { + name: acc.label, + editable: false, + deletable: false, + }); + }) + .filter(Boolean); + + children.push( + SidebarItem.forCategories(groupCats, { + name: group.name, + children: groupChildren, + editable: false, + deletable: false, + nameStyle: this._nameStyleForGroup(group), + }) + ); + } + + // Add ungrouped accounts as individual children + accounts.forEach(acc => { + if (groupedAccountIds.has(acc.id)) return; + const cat = _.first( + _.compact(names.map(name => CategoryStore.getCategoryByRole(acc, name))) + ); + if (!cat) return; + children.push( + SidebarItem.forCategories([cat], { name: acc.label, editable: false, deletable: false }) + ); + }); + + return children; + } + + static _buildGroupedPseudoChildren( + accounts: Account[], + groups: AccountGroup[], + itemFactory: (accountIds: string[], opts?: Partial) => ISidebarItem + ): ISidebarItem[] { + const children: ISidebarItem[] = []; + const groupedAccountIds = new Set(); + + for (const group of groups) { + const groupAccounts = accounts.filter(acc => group.accountIds.includes(acc.id)); + if (groupAccounts.length === 0) continue; + + groupAccounts.forEach(acc => groupedAccountIds.add(acc.id)); + + const groupIds = groupAccounts.map(acc => acc.id); + const groupChildren = groupAccounts.map(acc => + itemFactory([acc.id], { name: acc.label }) + ); + + children.push( + itemFactory(groupIds, { + name: group.name, + children: groupChildren, + nameStyle: this._nameStyleForGroup(group), + }) + ); + } + + // Add ungrouped accounts as individual children + accounts.forEach(acc => { + if (groupedAccountIds.has(acc.id)) return; + children.push(itemFactory([acc.id], { name: acc.label })); + }); + + return children; + } + static standardSectionForAccounts(accounts?: Account[]): ISidebarSection { let children; if (!accounts || accounts.length === 0) { @@ -95,6 +212,9 @@ class SidebarSection { return this.standardSectionForAccount(accounts[0]); } + const groups = AccountGroupStore.groups(); + const hasGroups = groups.length > 0; + const standardNames = ['inbox', 'important', 'sent', ['archive', 'all'], 'spam', 'trash']; const items = []; @@ -105,19 +225,23 @@ class SidebarSection { continue; } - children = []; - // eslint-disable-next-line - accounts.forEach(acc => { - const cat = _.first( - _.compact((names as string[]).map(name => CategoryStore.getCategoryByRole(acc, name))) - ); - if (!cat) { - return; - } - children.push( - SidebarItem.forCategories([cat], { name: acc.label, editable: false, deletable: false }) - ); - }); + if (hasGroups) { + children = this._buildGroupedCategoryChildren(accounts, names, groups); + } else { + children = []; + // eslint-disable-next-line + accounts.forEach(acc => { + const cat = _.first( + _.compact((names as string[]).map(name => CategoryStore.getCategoryByRole(acc, name))) + ); + if (!cat) { + return; + } + children.push( + SidebarItem.forCategories([cat], { name: acc.label, editable: false, deletable: false }) + ); + }); + } items.push( SidebarItem.forCategories(categories, { children, editable: false, deletable: false }) @@ -126,15 +250,38 @@ class SidebarSection { const accountIds = _.pluck(accounts, 'id'); - const starredItem = SidebarItem.forStarred(accountIds, { - children: accounts.map(acc => SidebarItem.forStarred([acc.id], { name: acc.label })), - }); - const unreadItem = SidebarItem.forUnread(accountIds, { - children: accounts.map(acc => SidebarItem.forUnread([acc.id], { name: acc.label })), - }); - const draftsItem = SidebarItem.forDrafts(accountIds, { - children: accounts.map(acc => SidebarItem.forDrafts([acc.id], { name: acc.label })), - }); + let starredChildren, unreadChildren, draftsChildren; + if (hasGroups) { + starredChildren = this._buildGroupedPseudoChildren( + accounts, + groups, + SidebarItem.forStarred.bind(SidebarItem) + ); + unreadChildren = this._buildGroupedPseudoChildren( + accounts, + groups, + SidebarItem.forUnread.bind(SidebarItem) + ); + draftsChildren = this._buildGroupedPseudoChildren( + accounts, + groups, + SidebarItem.forDrafts.bind(SidebarItem) + ); + } else { + starredChildren = accounts.map(acc => + SidebarItem.forStarred([acc.id], { name: acc.label }) + ); + unreadChildren = accounts.map(acc => + SidebarItem.forUnread([acc.id], { name: acc.label }) + ); + draftsChildren = accounts.map(acc => + SidebarItem.forDrafts([acc.id], { name: acc.label }) + ); + } + + const starredItem = SidebarItem.forStarred(accountIds, { children: starredChildren }); + const unreadItem = SidebarItem.forUnread(accountIds, { children: unreadChildren }); + const draftsItem = SidebarItem.forDrafts(accountIds, { children: draftsChildren }); // Order correctly: Inbox, Unread, Starred, rest... , Drafts items.splice(1, 0, unreadItem, starredItem); diff --git a/app/internal_packages/account-sidebar/lib/sidebar-store.ts b/app/internal_packages/account-sidebar/lib/sidebar-store.ts index 2dad9ac5cb..214c1a6bae 100644 --- a/app/internal_packages/account-sidebar/lib/sidebar-store.ts +++ b/app/internal_packages/account-sidebar/lib/sidebar-store.ts @@ -4,6 +4,7 @@ import { Actions, Account, AccountStore, + AccountGroupStore, ThreadCountsStore, WorkspaceStore, OutboxStore, @@ -64,6 +65,7 @@ class SidebarStore extends MailspringStore { this.listenTo(Actions.setCollapsedSidebarItem, this._onSetCollapsedByName); this.listenTo(SidebarActions.setKeyCollapsed, this._onSetCollapsedByKey); this.listenTo(AccountStore, this._onAccountsChanged); + this.listenTo(AccountGroupStore, this._updateSections); this.listenTo(FocusedPerspectiveStore, this._onFocusedPerspectiveChanged); this.listenTo(WorkspaceStore, this._updateSections); this.listenTo(OutboxStore, this._updateSections); diff --git a/app/internal_packages/account-sidebar/lib/types.ts b/app/internal_packages/account-sidebar/lib/types.ts index 989ffe17d2..e6e86e53ab 100644 --- a/app/internal_packages/account-sidebar/lib/types.ts +++ b/app/internal_packages/account-sidebar/lib/types.ts @@ -1,3 +1,4 @@ +import React from 'react'; import { MailboxPerspective } from 'mailspring-exports'; export interface ISidebarItem { @@ -11,6 +12,7 @@ export interface ISidebarItem { selected: boolean; collapsed: boolean; counterStyle: string; + nameStyle?: React.CSSProperties; onDelete?: () => void; onEdited?: (item, name: string) => void; onCollapseToggled: () => void; diff --git a/app/internal_packages/preferences/lib/main.tsx b/app/internal_packages/preferences/lib/main.tsx index a1313685f1..dba3352b5c 100644 --- a/app/internal_packages/preferences/lib/main.tsx +++ b/app/internal_packages/preferences/lib/main.tsx @@ -25,6 +25,14 @@ export function activate() { order: 2, }) ); + PreferencesUIStore.registerPreferencesTab( + new PreferencesUIStore.TabItem({ + tabId: 'Account Groups', + displayName: localized('Account Groups'), + componentClassFn: () => require('./tabs/preferences-account-groups').default, + order: 2.5, + }) + ); PreferencesUIStore.registerPreferencesTab( new PreferencesUIStore.TabItem({ tabId: 'Subscription', diff --git a/app/internal_packages/preferences/lib/tabs/preferences-account-groups.tsx b/app/internal_packages/preferences/lib/tabs/preferences-account-groups.tsx new file mode 100644 index 0000000000..6c6ba661f7 --- /dev/null +++ b/app/internal_packages/preferences/lib/tabs/preferences-account-groups.tsx @@ -0,0 +1,524 @@ +import React, { Component } from 'react'; +import { + localized, + Actions, + Account, + AccountStore, + AccountGroupStore, + AccountGroup, + AccountGroupDisplayStyle, +} from 'mailspring-exports'; +import { RetinaImg } from 'mailspring-component-kit'; + +interface PreferencesAccountGroupsState { + groups: AccountGroup[]; + accounts: Account[]; + selectedGroupId: string | null; + editingGroupId: string | null; + editingName: string; + showCreateDialog: boolean; + newGroupName: string; + searchQuery: string; +} + +class PreferencesAccountGroups extends Component< + Record, + PreferencesAccountGroupsState +> { + static displayName = 'PreferencesAccountGroups'; + + _unsubscribers: Array<() => void> = []; + + constructor(props) { + super(props); + const groups = AccountGroupStore.groups(); + this.state = { + groups, + accounts: AccountStore.accounts(), + selectedGroupId: groups.length > 0 ? groups[0].id : null, + editingGroupId: null, + editingName: '', + showCreateDialog: false, + newGroupName: '', + searchQuery: '', + }; + } + + componentDidMount() { + this._unsubscribers.push(AccountGroupStore.listen(this._onStoreChanged)); + this._unsubscribers.push(AccountStore.listen(this._onStoreChanged)); + } + + componentWillUnmount() { + this._unsubscribers.forEach(unsub => unsub()); + } + + _onStoreChanged = () => { + const groups = AccountGroupStore.groups(); + this.setState(prev => { + let selectedGroupId = prev.selectedGroupId; + if (!selectedGroupId || !groups.find(g => g.id === selectedGroupId)) { + selectedGroupId = groups.length > 0 ? groups[0].id : null; + } + return { + groups, + accounts: AccountStore.accounts(), + selectedGroupId, + }; + }); + }; + + _selectedGroup(): AccountGroup | null { + if (!this.state.selectedGroupId) return null; + return this.state.groups.find(g => g.id === this.state.selectedGroupId) || null; + } + + // Group CRUD + + _onOpenCreateDialog = () => { + this.setState({ showCreateDialog: true, newGroupName: '' }); + }; + + _onConfirmCreate = () => { + const name = this.state.newGroupName.trim(); + if (!name) return; + Actions.createAccountGroup({ name, accountIds: [] }); + this.setState({ showCreateDialog: false, newGroupName: '' }); + setTimeout(() => { + const groups = AccountGroupStore.groups(); + const newGroup = groups[groups.length - 1]; + if (newGroup) { + this.setState({ selectedGroupId: newGroup.id }); + } + }, 50); + }; + + _onCancelCreate = () => { + this.setState({ showCreateDialog: false, newGroupName: '' }); + }; + + _onDeleteGroup = (groupId: string) => { + const group = this.state.groups.find(g => g.id === groupId); + if (!group) return; + const remote = require('@electron/remote'); + const response = remote.dialog.showMessageBoxSync({ + type: 'warning', + message: localized('Delete Group'), + detail: localized('Are you sure you want to delete the group "%@"?', group.name), + buttons: [localized('Delete'), localized('Cancel')], + defaultId: 1, + }); + if (response !== 0) return; + Actions.deleteAccountGroup({ id: groupId }); + }; + + _onStartEditName = (group: AccountGroup) => { + this.setState({ editingGroupId: group.id, editingName: group.name }); + }; + + _onConfirmEditName = () => { + const name = this.state.editingName.trim(); + if (name && this.state.editingGroupId) { + Actions.updateAccountGroup({ id: this.state.editingGroupId, name }); + } + this.setState({ editingGroupId: null, editingName: '' }); + }; + + _onCancelEditName = () => { + this.setState({ editingGroupId: null, editingName: '' }); + }; + + // Account assignment + + _onToggleAccount = (accountId: string, checked: boolean) => { + const group = this._selectedGroup(); + if (!group) return; + const accountIds = checked + ? [...group.accountIds, accountId] + : group.accountIds.filter(id => id !== accountId); + Actions.updateAccountGroup({ id: group.id, accountIds }); + }; + + _onSelectAll = () => { + const group = this._selectedGroup(); + if (!group) return; + const accountIds = this._filteredAccounts().map(a => a.id); + const merged = new Set([...group.accountIds, ...accountIds]); + Actions.updateAccountGroup({ id: group.id, accountIds: Array.from(merged) }); + }; + + _onDeselectAll = () => { + const group = this._selectedGroup(); + if (!group) return; + const filteredIds = new Set(this._filteredAccounts().map(a => a.id)); + const remaining = group.accountIds.filter(id => !filteredIds.has(id)); + Actions.updateAccountGroup({ id: group.id, accountIds: remaining }); + }; + + // Customization + + _onSetColor = (color: string) => { + const group = this._selectedGroup(); + if (!group) return; + Actions.updateAccountGroup({ id: group.id, color }); + }; + + _onResetColor = () => { + const group = this._selectedGroup(); + if (!group) return; + Actions.updateAccountGroup({ id: group.id, color: '' }); + }; + + _onSetDisplayStyle = (displayStyle: AccountGroupDisplayStyle) => { + const group = this._selectedGroup(); + if (!group) return; + Actions.updateAccountGroup({ id: group.id, displayStyle }); + }; + + _getGroupNameStyle(group: AccountGroup): React.CSSProperties { + const style: React.CSSProperties = {}; + if (group.color) { + style.color = group.color; + } + const ds = group.displayStyle || 'normal'; + if (ds === 'bold' || ds === 'bold-italic') { + style.fontWeight = 700; + } + if (ds === 'italic' || ds === 'bold-italic') { + style.fontStyle = 'italic'; + } + return style; + } + + // Filtering + + _filteredAccounts() { + const query = this.state.searchQuery.toLowerCase().trim(); + if (!query) return this.state.accounts; + return this.state.accounts.filter( + acc => + (acc.label || '').toLowerCase().includes(query) || + acc.emailAddress.toLowerCase().includes(query) || + (acc.name || '').toLowerCase().includes(query) + ); + } + + // Create dialog + + _renderCreateDialog() { + if (!this.state.showCreateDialog) return null; + const canSave = this.state.newGroupName.trim().length > 0; + + return ( +
+
e.stopPropagation()}> +

{localized('Create New Group')}

+

{localized('Enter a name for the new account group.')}

+ this.setState({ newGroupName: e.target.value })} + onKeyDown={e => { + if (e.key === 'Enter' && canSave) this._onConfirmCreate(); + if (e.key === 'Escape') this._onCancelCreate(); + }} + autoFocus + /> +
+ + +
+
+
+ ); + } + + // Left panel renderers + + _renderGroupListItem(group: AccountGroup) { + const isSelected = this.state.selectedGroupId === group.id; + const isEditing = this.state.editingGroupId === group.id; + const accountCount = group.accountIds.length; + const borderStyle: React.CSSProperties = group.color + ? { borderLeftColor: group.color, borderLeftWidth: 3, borderLeftStyle: 'solid' } + : {}; + + return ( +
this.setState({ selectedGroupId: group.id })} + > + {isEditing ? ( + this.setState({ editingName: e.target.value })} + onKeyDown={e => { + if (e.key === 'Enter') this._onConfirmEditName(); + if (e.key === 'Escape') this._onCancelEditName(); + }} + onBlur={this._onConfirmEditName} + onClick={e => e.stopPropagation()} + autoFocus + /> + ) : ( +
+ + {group.name} + + + {accountCount} {accountCount === 1 ? localized('account') : localized('accounts')} + +
+ )} +
+ ); + } + + _renderLeftPanel() { + const { groups } = this.state; + + return ( +
+
+

{localized('Groups')}

+ +
+ +
+ {groups.map(group => this._renderGroupListItem(group))} +
+ + {groups.length === 0 && ( +
+

{localized('No groups yet.')}

+

{localized('Click "+ Add" to create one.')}

+
+ )} +
+ ); + } + + // Right panel renderers + + _renderGroupCustomization(group: AccountGroup) { + const displayStyle = group.displayStyle || 'normal'; + + return ( +
+
+ +
+ this._onSetColor(e.target.value)} + /> + + {group.name} + + {group.color && ( + + )} +
+
+
+ +
+ + + + +
+
+
+ ); + } + + _renderRightPanel() { + const group = this._selectedGroup(); + if (!group) { + return ( +
+
+

{localized('No Group Selected')}

+

+ {localized( + 'Create a group using the "+ Add" button, then select it to assign accounts.' + )} +

+
+
+ ); + } + + const filteredAccounts = this._filteredAccounts(); + const allSelected = + filteredAccounts.length > 0 && filteredAccounts.every(a => group.accountIds.includes(a.id)); + const someSelected = + !allSelected && filteredAccounts.some(a => group.accountIds.includes(a.id)); + + return ( +
+
+
+

{group.name}

+
+ + +
+
+ {this._renderGroupCustomization(group)} +
+ +
+
+
+ + this.setState({ searchQuery: e.target.value })} + /> + {this.state.searchQuery && ( + + )} +
+ +
+ +
+ {filteredAccounts.map(account => { + const checked = group.accountIds.includes(account.id); + return ( + + ); + })} + {filteredAccounts.length === 0 && ( +
+ {localized('No accounts match your search.')} +
+ )} +
+
+
+ ); + } + + render() { + return ( +
+
+ {this._renderLeftPanel()} + {this._renderRightPanel()} +
+ {this._renderCreateDialog()} +
+ ); + } +} + +export default PreferencesAccountGroups; diff --git a/app/internal_packages/preferences/lib/tabs/preferences-accounts.tsx b/app/internal_packages/preferences/lib/tabs/preferences-accounts.tsx index c3861b8d66..54b0a59fb1 100644 --- a/app/internal_packages/preferences/lib/tabs/preferences-accounts.tsx +++ b/app/internal_packages/preferences/lib/tabs/preferences-accounts.tsx @@ -1,12 +1,23 @@ import React from 'react'; import { ipcRenderer } from 'electron'; -import { AccountStore, Actions, Account } from 'mailspring-exports'; +import { + AccountStore, + AccountGroupStore, + AccountGroup, + Actions, + Account, + localized, +} from 'mailspring-exports'; import PreferencesAccountList from './preferences-account-list'; import PreferencesAccountDetails from './preferences-account-details'; interface PreferencesAccountsState { accounts: Account[]; selected: Account; + groups: AccountGroup[]; + showGroupPicker: boolean; + selectedGroupId: string; + previousAccountIds: string[]; } class PreferencesAccounts extends React.Component< @@ -15,21 +26,26 @@ class PreferencesAccounts extends React.Component< > { static displayName = 'PreferencesAccounts'; - unsubscribe: () => void; + _unsubscribers: Array<() => void> = []; constructor(props) { super(props); - this.state = this.getStateFromStores(); + this.state = { + ...this.getStateFromStores(), + groups: AccountGroupStore.groups(), + showGroupPicker: false, + selectedGroupId: '', + previousAccountIds: [], + }; } componentDidMount() { - this.unsubscribe = AccountStore.listen(this._onAccountsChanged); + this._unsubscribers.push(AccountStore.listen(this._onAccountsChanged)); + this._unsubscribers.push(AccountGroupStore.listen(this._onGroupsChanged)); } componentWillUnmount() { - if (this.unsubscribe) { - this.unsubscribe(); - } + this._unsubscribers.forEach(unsub => unsub()); } getStateFromStores({ selected }: { selected?: Account } = {}) { @@ -38,8 +54,6 @@ class PreferencesAccounts extends React.Component< if (selected) { selectedAccount = accounts.find(a => a.id === selected.id); } - // If selected was null or no longer exists in the AccountStore, - // just use the first account. if (!selectedAccount) { selectedAccount = accounts[0]; } @@ -50,13 +64,56 @@ class PreferencesAccounts extends React.Component< } _onAccountsChanged = () => { - this.setState(this.getStateFromStores(this.state)); + const prev = this.state.accounts; + const selectedGroupId = this.state.selectedGroupId; + const storeState = this.getStateFromStores(this.state); + this.setState(storeState as any); + + // If a new account was added and we had a group selected, assign it + if (selectedGroupId && storeState.accounts.length > prev.length) { + const prevIds = new Set(prev.map(a => a.id)); + const newAccounts = storeState.accounts.filter(a => !prevIds.has(a.id)); + if (newAccounts.length > 0) { + const group = AccountGroupStore.groupForId(selectedGroupId); + if (group) { + const newIds = [...group.accountIds, ...newAccounts.map(a => a.id)]; + Actions.updateAccountGroup({ id: group.id, accountIds: newIds }); + } + this.setState({ selectedGroupId: '' }); + } + } + }; + + _onGroupsChanged = () => { + this.setState({ groups: AccountGroupStore.groups() }); }; - // Update account list actions - _onAddAccount() { + _onAddAccount = () => { + const groups = AccountGroupStore.groups(); + if (groups.length > 0) { + this.setState({ + showGroupPicker: true, + selectedGroupId: groups[0].id, + previousAccountIds: this.state.accounts.map(a => a.id), + }); + } else { + ipcRenderer.send('command', 'application:add-account'); + } + }; + + _onConfirmGroupAndAdd = () => { + this.setState({ showGroupPicker: false }); ipcRenderer.send('command', 'application:add-account'); - } + }; + + _onSkipGroupAndAdd = () => { + this.setState({ showGroupPicker: false, selectedGroupId: '' }); + ipcRenderer.send('command', 'application:add-account'); + }; + + _onCancelGroupPicker = () => { + this.setState({ showGroupPicker: false, selectedGroupId: '' }); + }; _onReorderAccount(account: Account, oldIdx: number, newIdx: number) { Actions.reorderAccount(account.id, newIdx); @@ -70,11 +127,45 @@ class PreferencesAccounts extends React.Component< Actions.removeAccount(account.id); } - // Update account actions _onAccountUpdated(account: Account, updates: Partial) { Actions.updateAccount(account.id, updates); } + _renderGroupPicker() { + if (!this.state.showGroupPicker) return null; + + return ( +
+
+

{localized('Add Account to Group')}

+

{localized('Select a group for the new account, or skip to add without a group.')}

+ +
+ + + +
+
+
+ ); + } + render() { return (
@@ -92,6 +183,7 @@ class PreferencesAccounts extends React.Component< onAccountUpdated={this._onAccountUpdated} />
+ {this._renderGroupPicker()} ); } diff --git a/app/internal_packages/preferences/styles/preferences-accounts.less b/app/internal_packages/preferences/styles/preferences-accounts.less index 5a6c6269a1..0bf21e78c6 100644 --- a/app/internal_packages/preferences/styles/preferences-accounts.less +++ b/app/internal_packages/preferences/styles/preferences-accounts.less @@ -5,6 +5,8 @@ .container-accounts { min-width: 420px; margin: 0 auto; + position: relative; + .accounts-content { display: flex; justify-content: center; @@ -128,5 +130,533 @@ } } } + + // Group picker modal overlay + .group-picker-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .group-picker-modal { + background: @background-primary; + border-radius: @border-radius-large; + padding: @spacing-standard * 2; + width: 380px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + + h3 { + margin-top: 0; + margin-bottom: @spacing-standard * 0.5; + } + + p { + font-size: @font-size-small; + color: @text-color-subtle; + margin-bottom: @spacing-standard; + } + + .group-picker-select { + width: 100%; + padding: 8px; + font-size: @font-size; + margin-bottom: @spacing-standard; + border: 1px solid @border-color-divider; + border-radius: @border-radius-base; + } + + .group-picker-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + + .btn { + padding: 6px 14px; + } + } + } + } + + // Account Groups Tab - Split Panel Layout + .container-account-groups { + max-width: 800px; + min-width: 500px; + margin: 0 auto; + + .groups-split-layout { + display: flex; + background: @background-primary; + border: 1px solid darken(@border-color-divider, 8%); + border-radius: @border-radius-base; + box-shadow: @standard-shadow; + overflow: hidden; + // Use a fixed height so both panels scroll independently + height: 520px; + } + + // Left panel - Group list + .groups-left-panel { + width: 220px; + min-width: 180px; + flex-shrink: 0; + border-right: 1px solid darken(@border-color-divider, 8%); + background: @background-secondary; + display: flex; + flex-direction: column; + + .groups-left-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid darken(@border-color-divider, 8%); + background: darken(@background-secondary, 3%); + + h3 { + margin: 0; + font-size: @font-size; + font-weight: @headings-font-weight; + color: @text-color-heading; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .btn-add-group { + font-size: @font-size-small; + font-weight: @font-weight-semi-bold; + padding: 4px 10px; + white-space: nowrap; + } + } + + .groups-list { + flex: 1; + overflow-y: auto; + } + + .group-list-item { + padding: 10px 14px; + cursor: pointer; + border-bottom: 1px solid darken(@border-color-divider, 4%); + border-left: 3px solid transparent; + + &:hover { + background: darken(@background-secondary, 5%); + } + + &.selected { + background: @background-primary; + border-left-color: @component-active-color; + box-shadow: inset -1px 0 0 darken(@border-color-divider, 4%); + } + + &.new-group-input { + background: @background-primary; + border-left-color: @component-active-color; + } + + .group-list-item-content { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + } + + .group-name { + font-weight: @font-weight-semi-bold; + font-size: @font-size; + color: @text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .group-account-count { + font-size: @font-size-smaller; + color: @text-color-very-subtle; + } + + .group-name-input { + width: 100%; + font-size: @font-size; + font-weight: @font-weight-semi-bold; + padding: 4px 6px; + border: 1px solid @component-active-color; + border-radius: @border-radius-base; + outline: none; + box-sizing: border-box; + } + } + + .groups-empty-hint { + padding: 24px 14px; + text-align: center; + color: @text-color-very-subtle; + font-size: @font-size-smaller; + + p { + margin: 4px 0; + color: @text-color-very-subtle; + } + } + } + + // Right panel - Group details & accounts + .groups-right-panel { + flex: 1; + display: flex; + flex-direction: column; + background: @background-primary; + min-width: 0; + + .right-panel-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 40px 20px; + text-align: center; + + h3 { + margin: 0 0 8px 0; + font-size: @font-size-large; + font-weight: @headings-font-weight; + color: @text-color-heading; + } + + p { + max-width: 280px; + line-height: 1.5; + color: @text-color-very-subtle; + font-size: @font-size-small; + } + } + + .right-panel-header { + border-bottom: 1px solid darken(@border-color-divider, 4%); + background: @background-secondary; + + .right-panel-title-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + + h3 { + margin: 0; + font-size: @font-size-large; + font-weight: @headings-font-weight; + color: @text-color; + } + + .right-panel-actions { + display: flex; + gap: 6px; + + .btn-danger { + color: @color-danger; + border-color: fade(@color-danger, 30%); + &:hover { + background: @color-danger; + color: @text-color-inverse; + border-color: @color-danger; + } + } + } + } + } + + .group-customization { + padding: 10px 16px; + border-top: 1px solid darken(@border-color-divider, 4%); + background: @background-secondary; + + .customization-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + } + + .customization-label { + font-size: @font-size-small; + font-weight: @font-weight-medium; + color: @text-color-very-subtle; + width: 50px; + flex-shrink: 0; + } + + .customization-control { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + } + + .color-control { + input[type='color'] { + width: 28px; + height: 28px; + padding: 0; + border: 1px solid darken(@border-color-divider, 10%); + border-radius: @border-radius-base; + cursor: pointer; + background: none; + } + + .color-preview { + font-size: @font-size; + margin-left: 4px; + color: @text-color; + } + + .btn-reset { + margin-left: auto; + font-size: @font-size-small; + } + } + + .style-control { + .btn-style { + min-width: 36px; + padding: 4px 8px; + font-size: @font-size-small; + border: 1px solid darken(@border-color-divider, 10%); + background: @background-primary; + color: @text-color; + cursor: pointer; + border-radius: @border-radius-base; + + &.active { + background: @component-active-color; + color: @text-color-inverse; + border-color: @component-active-color; + } + + &:hover:not(.active) { + background: darken(@background-primary, 5%); + } + } + } + } + + .right-panel-accounts { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + + .accounts-toolbar { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 16px; + border-bottom: 1px solid darken(@border-color-divider, 4%); + background: @background-off-primary; + + .search-container { + flex: 1; + position: relative; + display: flex; + align-items: center; + + .search-icon { + position: absolute; + left: 10px; + width: 16px; + height: 16px; + opacity: 0.5; + } + + .search-input { + width: 100%; + padding: 6px 28px 6px 32px; + font-size: @font-size; + border: 1px solid darken(@border-color-divider, 8%); + border-radius: @border-radius-base; + background: @background-primary; + color: @text-color; + outline: none; + &:focus { + border-color: @component-active-color; + } + } + + .search-clear { + position: absolute; + right: 6px; + background: none; + border: none; + font-size: @font-size; + cursor: pointer; + color: @text-color-very-subtle; + padding: 2px 6px; + &:hover { + color: @text-color; + } + } + } + + .select-all-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: @font-size-small; + font-weight: @font-weight-medium; + cursor: pointer; + color: @text-color-subtle; + white-space: nowrap; + + input[type='checkbox'] { + margin: 0; + } + + &:hover { + color: @text-color; + } + } + } + + .accounts-list { + flex: 1; + overflow-y: auto; + padding: 2px 0; + } + + .account-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 16px; + cursor: pointer; + transition: background 0.1s ease; + + &:hover { + background: darken(@background-primary, 3%); + } + + &.checked { + background: fade(@component-active-color, 8%); + &:hover { + background: fade(@component-active-color, 14%); + } + } + + input[type='checkbox'] { + margin: 0; + flex-shrink: 0; + } + + .account-info { + display: flex; + flex-direction: column; + min-width: 0; + + .account-label { + font-size: @font-size; + font-weight: @font-weight-medium; + color: @text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .account-email { + font-size: @font-size-smaller; + color: @text-color-very-subtle; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + .no-results { + padding: 24px 16px; + text-align: center; + color: @text-color-very-subtle; + font-size: @font-size-small; + } + } + } + + // Create group dialog overlay + .create-group-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .create-group-dialog { + background: @background-primary; + border-radius: @border-radius-large; + padding: 24px; + width: 380px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); + + h3 { + margin: 0 0 6px 0; + font-size: @font-size-large; + font-weight: @headings-font-weight; + color: @text-color; + } + + p { + margin: 0 0 16px 0; + font-size: @font-size-small; + color: @text-color-very-subtle; + } + + .create-group-input { + width: 100%; + padding: 8px 10px; + font-size: @font-size; + color: @text-color; + border: 1px solid darken(@border-color-divider, 10%); + border-radius: @border-radius-base; + outline: none; + box-sizing: border-box; + margin-bottom: 16px; + + &:focus { + border-color: @component-active-color; + } + } + + .create-group-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + + .btn { + padding: 6px 16px; + font-size: @font-size-small; + } + + .btn[disabled] { + opacity: 0.5; + cursor: not-allowed; + } + } + } } } diff --git a/app/package-lock.json b/app/package-lock.json index 5b9f6d1e98..0d7fe79bac 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -638,20 +638,21 @@ "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==" }, "node_modules/cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", + "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.0", + "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" }, "engines": { @@ -2115,9 +2116,9 @@ } }, "node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -2125,17 +2126,19 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -2742,9 +2745,9 @@ } }, "node_modules/juice/node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -3181,28 +3184,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, - "node_modules/node-abi": { - "version": "3.67.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", - "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", @@ -3547,6 +3528,30 @@ "node": ">=10" } }, + "node_modules/prebuild-install/node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -4869,9 +4874,9 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.2.tgz", + "integrity": "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -5653,20 +5658,20 @@ "integrity": "sha512-SxltgMwL9uCko5/ZCLiyG2B7R9fY4pDZUw7hJ4MhirdjBLosoDqkWABi3XMucddHdLiFJMb7PD2MZifZriuMTg==" }, "cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "requires": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", + "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.0", + "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, @@ -6747,20 +6752,20 @@ } }, "htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "requires": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" }, "dependencies": { "entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==" + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==" } } }, @@ -7182,9 +7187,9 @@ } }, "entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==" + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==" }, "htmlparser2": { "version": "9.1.0", @@ -7522,21 +7527,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, - "node-abi": { - "version": "3.67.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", - "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", - "requires": { - "semver": "^7.3.5" - }, - "dependencies": { - "semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" - } - } - }, "node-addon-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz", @@ -7770,12 +7760,27 @@ "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", + "node-abi": "^3.87.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "requires": { + "semver": "^7.3.5" + } + }, + "semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==" + } } }, "process-nextick-args": { @@ -8792,9 +8797,9 @@ } }, "undici": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", - "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==" + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.2.tgz", + "integrity": "sha512-4VQSpGEGsWzk0VYxyB/wVX/Q7qf9t5znLRgs0dzszr9w9Fej/8RVNQ+S20vdXSAyra/bJ7ZQfGv6ZMj7UEbzSg==" }, "undici-types": { "version": "6.21.0", diff --git a/app/package.json b/app/package.json index d1ab96223a..cab7739c5e 100644 --- a/app/package.json +++ b/app/package.json @@ -81,6 +81,9 @@ "windows-iana": "^4.2.1", "xml2js": "^0.6.2" }, + "overrides": { + "node-abi": "^3.87.0" + }, "resolutions": { "electron": "39.2.7" }, diff --git a/app/src/components/outline-view-item.tsx b/app/src/components/outline-view-item.tsx index 7982cb563f..c11a80eb98 100644 --- a/app/src/components/outline-view-item.tsx +++ b/app/src/components/outline-view-item.tsx @@ -339,7 +339,7 @@ class OutlineViewItem extends Component ) : ( -
+
{item.name}
)} diff --git a/app/src/components/outline-view.tsx b/app/src/components/outline-view.tsx index bdc3a16d43..ed9924ad11 100644 --- a/app/src/components/outline-view.tsx +++ b/app/src/components/outline-view.tsx @@ -10,6 +10,7 @@ export interface IOutlineViewItem { title?: string; iconName?: string; name?: string; + nameStyle?: CSSProperties; children?: IOutlineViewItem[]; contextMenuLabel?: string; collapsed?: boolean; diff --git a/app/src/flux/actions.ts b/app/src/flux/actions.ts index 9272783546..a3f7bddb85 100644 --- a/app/src/flux/actions.ts +++ b/app/src/flux/actions.ts @@ -545,6 +545,12 @@ export const searchQuerySubmitted = create('searchQuerySubmitted', ActionScopeWi export const searchQueryChanged = create('searchQueryChanged', ActionScopeWindow); export const searchCompleted = create('searchCompleted', ActionScopeWindow); +// Account Groups +export const createAccountGroup = create('createAccountGroup', ActionScopeWindow); +export const updateAccountGroup = create('updateAccountGroup', ActionScopeWindow); +export const deleteAccountGroup = create('deleteAccountGroup', ActionScopeWindow); +export const reorderAccountGroup = create('reorderAccountGroup', ActionScopeWindow); + // Calendar export const focusCalendarEvent = create('focusCalendarEvent', ActionScopeWindow); diff --git a/app/src/flux/stores/account-group-store.ts b/app/src/flux/stores/account-group-store.ts new file mode 100644 index 0000000000..4fc6c6e527 --- /dev/null +++ b/app/src/flux/stores/account-group-store.ts @@ -0,0 +1,112 @@ +import MailspringStore from 'mailspring-store'; +import * as Actions from '../actions'; +import * as Utils from '../models/utils'; +import AccountStore from './account-store'; + +const configKey = 'accountGroups'; + +export type AccountGroupDisplayStyle = 'normal' | 'bold' | 'italic' | 'bold-italic'; + +export interface AccountGroup { + id: string; + name: string; + accountIds: string[]; + color?: string; + displayStyle?: AccountGroupDisplayStyle; +} + +class _AccountGroupStore extends MailspringStore { + private _groups: AccountGroup[] = []; + + constructor() { + super(); + this._loadGroups(); + this.listenTo(Actions.createAccountGroup, this._onCreate); + this.listenTo(Actions.updateAccountGroup, this._onUpdate); + this.listenTo(Actions.deleteAccountGroup, this._onDelete); + this.listenTo(Actions.reorderAccountGroup, this._onReorder); + this.listenTo(AccountStore, this._onAccountsChanged); + } + + groups(): AccountGroup[] { + return this._groups; + } + + groupForId(id: string): AccountGroup | undefined { + return this._groups.find(g => g.id === id); + } + + private _loadGroups() { + this._groups = (AppEnv.config.get(configKey) as AccountGroup[]) || []; + } + + private _saveGroups() { + AppEnv.config.set(configKey, this._groups); + } + + private _onCreate = ({ name, accountIds }: { name: string; accountIds: string[] }) => { + this._groups = [ + ...this._groups, + { + id: Utils.generateTempId(), + name, + accountIds, + color: '', + displayStyle: 'normal', + }, + ]; + this._saveGroups(); + this.trigger(); + }; + + private _onUpdate = (updates: Partial & { id: string }) => { + this._groups = this._groups.map(g => { + if (g.id !== updates.id) return g; + const result = { ...g }; + if (updates.name !== undefined) result.name = updates.name; + if (updates.accountIds !== undefined) result.accountIds = updates.accountIds; + if (updates.color !== undefined) result.color = updates.color; + if (updates.displayStyle !== undefined) result.displayStyle = updates.displayStyle; + return result; + }); + this._saveGroups(); + this.trigger(); + }; + + private _onDelete = ({ id }: { id: string }) => { + this._groups = this._groups.filter(g => g.id !== id); + this._saveGroups(); + this.trigger(); + }; + + private _onAccountsChanged = () => { + const validIds = new Set(AccountStore.accounts().map(a => a.id)); + let changed = false; + const cleaned = this._groups.map(g => { + const filtered = g.accountIds.filter(id => validIds.has(id)); + if (filtered.length !== g.accountIds.length) { + changed = true; + return { ...g, accountIds: filtered }; + } + return g; + }); + if (changed) { + this._groups = cleaned; + this._saveGroups(); + this.trigger(); + } + }; + + private _onReorder = ({ id, newIdx }: { id: string; newIdx: number }) => { + const groups = [...this._groups]; + const oldIdx = groups.findIndex(g => g.id === id); + if (oldIdx === -1) return; + const [group] = groups.splice(oldIdx, 1); + groups.splice(newIdx, 0, group); + this._groups = groups; + this._saveGroups(); + this.trigger(); + }; +} + +export default new _AccountGroupStore(); diff --git a/app/src/global/mailspring-exports.d.ts b/app/src/global/mailspring-exports.d.ts index 37575949b4..536d7359e9 100644 --- a/app/src/global/mailspring-exports.d.ts +++ b/app/src/global/mailspring-exports.d.ts @@ -113,6 +113,9 @@ export const ContactStore: ContactStore; export * from '../flux/stores/identity-store'; export type CategoryStore = typeof import('../flux/stores/category-store').default; export const CategoryStore: CategoryStore; +export type AccountGroupStore = typeof import('../flux/stores/account-group-store').default; +export const AccountGroupStore: AccountGroupStore; +export * from '../flux/stores/account-group-store'; export type UndoRedoStore = typeof import('../flux/stores/undo-redo-store').default; export const UndoRedoStore: UndoRedoStore; export type WorkspaceStore = typeof import('../flux/stores/workspace-store').default; diff --git a/app/src/global/mailspring-exports.js b/app/src/global/mailspring-exports.js index dc83f8dd5a..9a86c8ee20 100644 --- a/app/src/global/mailspring-exports.js +++ b/app/src/global/mailspring-exports.js @@ -135,6 +135,7 @@ load(`ModalStore`, 'flux/stores/modal-store'); load(`OutboxStore`, 'flux/stores/outbox-store'); load(`PopoverStore`, 'flux/stores/popover-store'); load(`AccountStore`, 'flux/stores/account-store'); +load(`AccountGroupStore`, 'flux/stores/account-group-store'); load(`SignatureStore`, 'flux/stores/signature-store'); load(`MessageStore`, 'flux/stores/message-store'); load(`ContactStore`, 'flux/stores/contact-store'); diff --git a/app/src/key-manager.ts b/app/src/key-manager.ts index 4e0926b17d..2898dfc6d0 100644 --- a/app/src/key-manager.ts +++ b/app/src/key-manager.ts @@ -9,6 +9,7 @@ const { safeStorage } = require('@electron/remote'); const configCredentialsKey = 'credentials'; + /** * A basic wrap around electron's secure key management. Consolidates all of * our keys under a single namespaced keymap and provides migration diff --git a/package-lock.json b/package-lock.json index a200225825..0bfd736a69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,12 +60,9 @@ "glob": "7.x.x", "grunt": "^1.4.1", "grunt-cli": "^1.4.3", - "handlebars": "^4.7.6", "jasmine": "2.x.x", - "joanna": "^0.0.8", "license-extractor": "^1.0.4", "load-grunt-parent-tasks": "^0.1.2", - "meta-marked": "^0.4.2", "mkdirp": "^0.5.1", "node-gyp": "^12.1.0", "prettier": "^1.10.0", @@ -75,6 +72,9 @@ "typescript": "5.7.3", "underscore": "^1.13.1" }, + "devDependencies": { + "node-abi": "^3.87.0" + }, "engines": { "node": ">=16.17", "npm": ">=8" @@ -1677,14 +1677,6 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" }, - "node_modules/babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "bin": { - "babylon": "bin/babylon.js" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3792,26 +3784,6 @@ "nopt": "bin/nopt.js" } }, - "node_modules/handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -4358,18 +4330,6 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=" }, - "node_modules/joanna": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/joanna/-/joanna-0.0.8.tgz", - "integrity": "sha1-o1MqyOc6Nj2OHUI3usOpTnBFUYk=", - "dependencies": { - "babylon": "^6.8.4", - "walkdir": ">= 0.0.2" - }, - "bin": { - "joanna": "src/cli.js" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4677,17 +4637,6 @@ "node": ">=0.10.0" } }, - "node_modules/marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", - "bin": { - "marked": "bin/marked" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -4730,15 +4679,6 @@ "node": ">= 8" } }, - "node_modules/meta-marked": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/meta-marked/-/meta-marked-0.4.4.tgz", - "integrity": "sha512-nTDbU+HBmX23U+wXrghjVjNxWnCmXVAMhbZ2sw0aBBcBUT2Fi0632Cfo3U2osc7eWH5pP0VXfpH0CG4DNcYCHw==", - "dependencies": { - "js-yaml": "~3.14.0", - "marked": "~0.7.0" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4947,10 +4887,18 @@ "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } }, "node_modules/node-gyp": { "version": "12.1.0", @@ -6054,14 +6002,6 @@ "node": ">= 14" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -6603,18 +6543,6 @@ "node": ">=14.17" } }, - "node_modules/uglify-js": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.3.tgz", - "integrity": "sha512-otIc7O9LyxpUcQoXzj2hL4LPWKklO6LJWoJUzNa8A17Xgi4fOeDC8FBDOLHnC/Slo1CQgsZMcM6as0M76BZaig==", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -6738,14 +6666,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6805,11 +6725,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -8077,11 +7992,6 @@ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==" }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -9647,18 +9557,6 @@ } } }, - "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -10017,15 +9915,6 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz", "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=" }, - "joanna": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/joanna/-/joanna-0.0.8.tgz", - "integrity": "sha1-o1MqyOc6Nj2OHUI3usOpTnBFUYk=", - "requires": { - "babylon": "^6.8.4", - "walkdir": ">= 0.0.2" - } - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10271,11 +10160,6 @@ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" - }, "matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -10303,15 +10187,6 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, - "meta-marked": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/meta-marked/-/meta-marked-0.4.4.tgz", - "integrity": "sha512-nTDbU+HBmX23U+wXrghjVjNxWnCmXVAMhbZ2sw0aBBcBUT2Fi0632Cfo3U2osc7eWH5pP0VXfpH0CG4DNcYCHw==", - "requires": { - "js-yaml": "~3.14.0", - "marked": "~0.7.0" - } - }, "micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -10458,10 +10333,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } }, "node-gyp": { "version": "12.1.0", @@ -11211,11 +11090,6 @@ "socks": "^2.8.3" } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, "spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -11612,12 +11486,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==" }, - "uglify-js": { - "version": "3.13.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.3.tgz", - "integrity": "sha512-otIc7O9LyxpUcQoXzj2hL4LPWKklO6LJWoJUzNa8A17Xgi4fOeDC8FBDOLHnC/Slo1CQgsZMcM6as0M76BZaig==", - "optional": true - }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -11717,11 +11585,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11761,11 +11624,6 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 1e21aedb0b..fe39873746 100644 --- a/package.json +++ b/package.json @@ -84,5 +84,8 @@ "tsc-watch": "tsc -w -p ./app --noEmit", "build": "grunt build-client --gruntfile=app/build/Gruntfile.js --base=./", "build-licenses-file": "cat ./app/static/all_licenses_preamble.html | ./node_modules/.bin/marked > ./app/static/all_licenses.html && licext --source ./app --mode output | ./node_modules/.bin/marked >> ./app/static/all_licenses.html" + }, + "devDependencies": { + "node-abi": "^3.87.0" } } diff --git a/scripts/postinstall.js b/scripts/postinstall.js index de2af2e37e..b2e3357c9d 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -20,7 +20,7 @@ const npmEnvs = { npm_config_target_arch: process.env.OVERRIDE_TO_INTEL ? 'x64' : process.arch, npm_config_disturl: 'https://electronjs.org/headers', npm_config_runtime: 'electron', - npm_config_build_from_source: true, + npm_config_build_from_source: process.platform !== 'win32', }), };