From 4ec138da9dd07b1155fff34992386444602b0f89 Mon Sep 17 00:00:00 2001 From: Arjita Mitra Date: Mon, 12 Jan 2026 21:38:41 +0100 Subject: [PATCH 1/2] feat: refactor cells node share modal, add password, expiration section(WPB-20913) --- apps/webapp/src/i18n/en-US.json | 4 + .../ShareModal/CellsShareModalContent.tsx | 206 ++++++++++++++++++ .../ShareModal/useCellExpirationToggle.ts | 30 +++ .../Cells/ShareModal/useCellPasswordToggle.ts | 30 +++ .../CellsShareModal/CellsShareModal.styles.ts | 31 ++- .../CellsShareModal/CellsShareModal.tsx | 110 +++++----- .../CellsNodeShareModal.styles.ts | 31 ++- .../CellsNodeShareModal.tsx | 106 +++++---- apps/webapp/src/style/common/mixins.less | 2 +- apps/webapp/src/style/foundation/themes.less | 2 + apps/webapp/src/types/i18n.d.ts | 4 + 11 files changed, 455 insertions(+), 101 deletions(-) create mode 100644 apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx create mode 100644 apps/webapp/src/script/components/Cells/ShareModal/useCellExpirationToggle.ts create mode 100644 apps/webapp/src/script/components/Cells/ShareModal/useCellPasswordToggle.ts diff --git a/apps/webapp/src/i18n/en-US.json b/apps/webapp/src/i18n/en-US.json index fa3fdeb4a30..988d3afe517 100644 --- a/apps/webapp/src/i18n/en-US.json +++ b/apps/webapp/src/i18n/en-US.json @@ -501,6 +501,10 @@ "cells.shareModal.heading": "Share via link", "cells.shareModal.linkCopied": "Link copied", "cells.shareModal.primaryAction": "Done", + "cells.shareModal.password": "Password", + "cells.shareModal.password.description": "Set a password to restrict access.", + "cells.shareModal.expiration": "Expiration", + "cells.shareModal.expiration.description": "Link expires on a selected day.", "cells.sidebar.heading": "Files", "cells.sidebar.title": "All", "cells.tableRow.actions": "More options", diff --git a/apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx b/apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx new file mode 100644 index 00000000000..e114dfc4da6 --- /dev/null +++ b/apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx @@ -0,0 +1,206 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {ComponentProps} from 'react'; +import {CSSObject} from '@emotion/react'; +import {BASE_DARK_COLOR, BASE_LIGHT_COLOR, COLOR_V2, Input, Label, Switch} from '@wireapp/react-ui-kit'; + +import {CellsTableLoader} from 'Components/Conversation/ConversationCells/common/CellsTableLoader/CellsTableLoader'; +import {CopyToClipboardButton} from 'Components/CopyToClipboardButton/CopyToClipboardButton'; + +type PublicLinkStatus = 'idle' | 'loading' | 'error' | 'success'; + +type SwitchColorProps = Pick< + ComponentProps, + | 'activatedColor' + | 'activatedColorDark' + | 'deactivatedColor' + | 'deactivatedColorDark' + | 'disabledColor' + | 'disabledColorDark' +>; + +interface CellsShareModalContentStyles { + wrapperStyles: CSSObject; + labelStyles: CSSObject; + publicLinkDescriptionStyles: CSSObject; + passwordDescriptionStyles: CSSObject; + expirationDescriptionStyles: CSSObject; + dividerStyles: CSSObject; + switchContentStyles: CSSObject; + toggleContentStyles: CSSObject; + switchContainerStyles: CSSObject; + switchWrapperStyles: CSSObject; + inputStyles: CSSObject; + inputWrapperStyles: CSSObject; + loaderWrapperStyles: CSSObject; +} + +interface CellsShareModalContentProps { + publicLinkDescription: string; + labels: { + enablePublicLink: string; + password: string; + passwordDescription: string; + expiration: string; + expirationDescription: string; + generatedPublicLink: string; + copyLink: string; + linkCopied: string; + errorLoadingLink: string; + }; + publicLink: { + status: PublicLinkStatus; + link?: string; + isEnabled: boolean; + onToggle: () => void; + disabled?: boolean; + }; + password: { + isEnabled: boolean; + onToggle: () => void; + }; + expiration: { + isEnabled: boolean; + onToggle: () => void; + }; + isInputDisabled: boolean; + styles: CellsShareModalContentStyles; + switchColors?: { + publicLink?: SwitchColorProps; + password?: SwitchColorProps; + expiration?: SwitchColorProps; + }; +} + +const DEFAULT_SWITCH_COLORS: SwitchColorProps = { + activatedColor: BASE_LIGHT_COLOR.GREEN, + activatedColorDark: BASE_DARK_COLOR.GREEN, + deactivatedColor: COLOR_V2.GRAY_70, + deactivatedColorDark: COLOR_V2.GRAY_60, + disabledColor: COLOR_V2.GRAY_70, + disabledColorDark: COLOR_V2.GRAY_60, +}; + +export const CellsShareModalContent = ({ + publicLinkDescription, + labels, + publicLink, + password, + expiration, + isInputDisabled, + styles, + switchColors, +}: CellsShareModalContentProps) => { + const shouldShowLink = publicLink.isEnabled && publicLink.status === 'success' && publicLink.link; + const publicLinkColors = switchColors?.publicLink ?? DEFAULT_SWITCH_COLORS; + const passwordColors = switchColors?.password ?? DEFAULT_SWITCH_COLORS; + const expirationColors = switchColors?.expiration ?? DEFAULT_SWITCH_COLORS; + + return ( +
+
+
+ + +
+
+ +
+
+
+
+
+ +

+ {labels.passwordDescription} +

+
+
+ +
+
+ {password.isEnabled &&
} +
+
+ +

+ {labels.expirationDescription} +

+
+
+ +
+
+ {expiration.isEnabled &&
} + {shouldShowLink && ( +
+ + + +
+ )} + {publicLink.status === 'loading' && ( +
+ +
+ )} + {publicLink.status === 'error' &&
{labels.errorLoadingLink}
} +
+ ); +}; diff --git a/apps/webapp/src/script/components/Cells/ShareModal/useCellExpirationToggle.ts b/apps/webapp/src/script/components/Cells/ShareModal/useCellExpirationToggle.ts new file mode 100644 index 00000000000..6e052c8825d --- /dev/null +++ b/apps/webapp/src/script/components/Cells/ShareModal/useCellExpirationToggle.ts @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useState} from 'react'; + +export const useCellExpirationToggle = () => { + const [isEnabled, setIsEnabled] = useState(false); + + const toggle = useCallback(() => { + setIsEnabled(prev => !prev); + }, []); + + return {isEnabled, toggle}; +}; diff --git a/apps/webapp/src/script/components/Cells/ShareModal/useCellPasswordToggle.ts b/apps/webapp/src/script/components/Cells/ShareModal/useCellPasswordToggle.ts new file mode 100644 index 00000000000..bff519d143b --- /dev/null +++ b/apps/webapp/src/script/components/Cells/ShareModal/useCellPasswordToggle.ts @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {useCallback, useState} from 'react'; + +export const useCellPasswordToggle = () => { + const [isEnabled, setIsEnabled] = useState(false); + + const toggle = useCallback(() => { + setIsEnabled(prev => !prev); + }, []); + + return {isEnabled, toggle}; +}; diff --git a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts index 2445a525cb8..a039ee48511 100644 --- a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts +++ b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts @@ -19,6 +19,8 @@ import {CSSObject} from '@emotion/react'; +import {COLOR_V2} from '@wireapp/react-ui-kit'; + export const wrapperStyles: CSSObject = { paddingTop: '8px', width: '100%', @@ -28,11 +30,38 @@ export const labelStyles: CSSObject = { fontSize: 'var(--font-size-base)', fontWeight: 'var(--font-weight-regular)', marginBottom: '8px', + color: 'var(--main-color)', }; export const publicLinkDescriptionStyles: CSSObject = { fontSize: 'var(--font-size-small)', - color: 'var(--gray-70)', + color: 'var(--base-secondary-text)', +}; + +export const passwordDescriptionStyles: CSSObject = { + fontSize: 'var(--font-size-small)', + color: COLOR_V2.GRAY_90, +}; + +export const expirationDescriptionStyles: CSSObject = { + fontSize: 'var(--font-size-small)', + color: COLOR_V2.GRAY_90, +}; + +export const dividerStyles: CSSObject = { + border: 0, + height: '1px', + backgroundColor: '#DCE0E3', + margin: '16px 0', +}; + +export const switchContentStyles: CSSObject = { + flex: 1, +}; + +export const toggleContentStyles: CSSObject = { + minHeight: '32px', + marginBottom: '16px', }; export const switchContainerStyles: CSSObject = { diff --git a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx index e9aca11e050..87e45cef1ce 100644 --- a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx +++ b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx @@ -17,9 +17,9 @@ * */ -import {Input, Label, Switch} from '@wireapp/react-ui-kit'; - -import {CopyToClipboardButton} from 'Components/CopyToClipboardButton/CopyToClipboardButton'; +import {CellsShareModalContent} from 'Components/Cells/ShareModal/CellsShareModalContent'; +import {useCellExpirationToggle} from 'Components/Cells/ShareModal/useCellExpirationToggle'; +import {useCellPasswordToggle} from 'Components/Cells/ShareModal/useCellPasswordToggle'; import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import {CellsRepository} from 'Repositories/cells/CellsRepository'; import {t} from 'Util/LocalizerUtil'; @@ -29,15 +29,18 @@ import { inputWrapperStyles, labelStyles, loaderWrapperStyles, + dividerStyles, publicLinkDescriptionStyles, + passwordDescriptionStyles, + expirationDescriptionStyles, + switchContentStyles, switchContainerStyles, switchWrapperStyles, + toggleContentStyles, wrapperStyles, } from './CellsShareModal.styles'; import {useCellPublicLink} from './useCellPublicLink'; -import {CellsTableLoader} from '../../../common/CellsTableLoader/CellsTableLoader'; - interface ShareModalParams { type: 'file' | 'folder'; uuid: string; @@ -49,61 +52,68 @@ export const showShareModal = ({type, uuid, cellsRepository}: ShareModalParams) size: 'large', primaryAction: {action: () => {}, text: t('cells.shareModal.primaryAction')}, text: { - message: , + message: , title: t('cells.shareModal.heading'), }, }); }; -const CellsShareModalContent = ({type, uuid, cellsRepository}: ShareModalParams) => { +const CellsShareModal = ({type, uuid, cellsRepository}: ShareModalParams) => { const {status, link, isEnabled, togglePublicLink} = useCellPublicLink({uuid, cellsRepository}); + const {isEnabled: isPasswordEnabled, toggle: togglePassword} = useCellPasswordToggle(); + const {isEnabled: isExpirationEnabled, toggle: toggleExpiration} = useCellExpirationToggle(); const isInputDisabled = ['loading', 'error'].includes(status); return ( -
-
-
- - -
-
- -
-
- {isEnabled && status === 'success' && link && ( -
- - - -
- )} - {status === 'loading' && ( -
- -
+ {t('cells.shareModal.error.loadingLink')}
} -
+ labels={{ + enablePublicLink: t('cells.shareModal.enablePublicLink'), + password: t('cells.shareModal.password'), + passwordDescription: t('cells.shareModal.password.description'), + expiration: t('cells.shareModal.expiration'), + expirationDescription: t('cells.shareModal.expiration.description'), + generatedPublicLink: t('cells.shareModal.generatedPublicLink'), + copyLink: t('cells.shareModal.copyLink'), + linkCopied: t('cells.shareModal.linkCopied'), + errorLoadingLink: t('cells.shareModal.error.loadingLink'), + }} + publicLink={{ + status, + link, + isEnabled, + onToggle: togglePublicLink, + disabled: status === 'loading', + }} + password={{ + isEnabled: isPasswordEnabled, + onToggle: togglePassword, + }} + expiration={{ + isEnabled: isExpirationEnabled, + onToggle: toggleExpiration, + }} + isInputDisabled={isInputDisabled} + styles={{ + wrapperStyles, + labelStyles, + publicLinkDescriptionStyles, + passwordDescriptionStyles, + expirationDescriptionStyles, + dividerStyles, + switchContentStyles, + toggleContentStyles, + switchContainerStyles, + switchWrapperStyles, + inputStyles, + inputWrapperStyles, + loaderWrapperStyles, + }} + /> ); }; diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts index 2445a525cb8..a039ee48511 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts @@ -19,6 +19,8 @@ import {CSSObject} from '@emotion/react'; +import {COLOR_V2} from '@wireapp/react-ui-kit'; + export const wrapperStyles: CSSObject = { paddingTop: '8px', width: '100%', @@ -28,11 +30,38 @@ export const labelStyles: CSSObject = { fontSize: 'var(--font-size-base)', fontWeight: 'var(--font-weight-regular)', marginBottom: '8px', + color: 'var(--main-color)', }; export const publicLinkDescriptionStyles: CSSObject = { fontSize: 'var(--font-size-small)', - color: 'var(--gray-70)', + color: 'var(--base-secondary-text)', +}; + +export const passwordDescriptionStyles: CSSObject = { + fontSize: 'var(--font-size-small)', + color: COLOR_V2.GRAY_90, +}; + +export const expirationDescriptionStyles: CSSObject = { + fontSize: 'var(--font-size-small)', + color: COLOR_V2.GRAY_90, +}; + +export const dividerStyles: CSSObject = { + border: 0, + height: '1px', + backgroundColor: '#DCE0E3', + margin: '16px 0', +}; + +export const switchContentStyles: CSSObject = { + flex: 1, +}; + +export const toggleContentStyles: CSSObject = { + minHeight: '32px', + marginBottom: '16px', }; export const switchContainerStyles: CSSObject = { diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx index ddbe011e723..b7d9f442aa9 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx @@ -17,9 +17,9 @@ * */ -import {Input, Label, Switch} from '@wireapp/react-ui-kit'; - -import {CopyToClipboardButton} from 'Components/CopyToClipboardButton/CopyToClipboardButton'; +import {CellsShareModalContent} from 'Components/Cells/ShareModal/CellsShareModalContent'; +import {useCellExpirationToggle} from 'Components/Cells/ShareModal/useCellExpirationToggle'; +import {useCellPasswordToggle} from 'Components/Cells/ShareModal/useCellPasswordToggle'; import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import {CellsRepository} from 'Repositories/cells/CellsRepository'; import {t} from 'Util/LocalizerUtil'; @@ -29,15 +29,18 @@ import { inputWrapperStyles, labelStyles, loaderWrapperStyles, + dividerStyles, publicLinkDescriptionStyles, + passwordDescriptionStyles, + expirationDescriptionStyles, + switchContentStyles, switchContainerStyles, switchWrapperStyles, + toggleContentStyles, wrapperStyles, } from './CellsNodeShareModal.styles'; import {useCellPublicLink} from './useCellPublicLink'; -import {CellsTableLoader} from '../../../common/CellsTableLoader/CellsTableLoader'; - interface ShareModalParams { type: 'file' | 'folder'; uuid: string; @@ -65,53 +68,60 @@ export const showShareModal = ({type, uuid, conversationId, cellsRepository}: Sh const CellShareModalContent = ({type, uuid, conversationId, cellsRepository}: ShareModalParams) => { const {status, link, isEnabled, togglePublicLink} = useCellPublicLink({uuid, conversationId, cellsRepository}); + const {isEnabled: isPasswordEnabled, toggle: togglePassword} = useCellPasswordToggle(); + const {isEnabled: isExpirationEnabled, toggle: toggleExpiration} = useCellExpirationToggle(); const isInputDisabled = ['loading', 'error'].includes(status); return ( -
-
-
- - -
-
- -
-
- {isEnabled && status === 'success' && link && ( -
- - - -
- )} - {status === 'loading' && ( -
- -
+ {t('cells.shareModal.error.loadingLink')}
} -
+ labels={{ + enablePublicLink: t('cells.shareModal.enablePublicLink'), + password: t('cells.shareModal.password'), + passwordDescription: t('cells.shareModal.password.description'), + expiration: t('cells.shareModal.expiration'), + expirationDescription: t('cells.shareModal.expiration.description'), + generatedPublicLink: t('cells.shareModal.generatedPublicLink'), + copyLink: t('cells.shareModal.copyLink'), + linkCopied: t('cells.shareModal.linkCopied'), + errorLoadingLink: t('cells.shareModal.error.loadingLink'), + }} + publicLink={{ + status, + link, + isEnabled, + onToggle: togglePublicLink, + disabled: status === 'loading', + }} + password={{ + isEnabled: isPasswordEnabled, + onToggle: togglePassword, + }} + expiration={{ + isEnabled: isExpirationEnabled, + onToggle: toggleExpiration, + }} + isInputDisabled={isInputDisabled} + styles={{ + wrapperStyles, + labelStyles, + publicLinkDescriptionStyles, + passwordDescriptionStyles, + expirationDescriptionStyles, + dividerStyles, + switchContentStyles, + toggleContentStyles, + switchContainerStyles, + switchWrapperStyles, + inputStyles, + inputWrapperStyles, + loaderWrapperStyles, + }} + /> ); }; diff --git a/apps/webapp/src/style/common/mixins.less b/apps/webapp/src/style/common/mixins.less index 39c65c6951e..03007bee7a5 100644 --- a/apps/webapp/src/style/common/mixins.less +++ b/apps/webapp/src/style/common/mixins.less @@ -41,7 +41,7 @@ .setVariables(foreground background); .setColorFade(foreground, @foreground); .setColorFade(background, @background); - .setVariables(app-bg sidebar-bg input-bar-bg transparent-img-bg sidebar-border-color sidebar-folder-selected-bg main-color border-color app-bg-secondary conversation-list-bg-opacity group-icon-bg); + .setVariables(app-bg sidebar-bg input-bar-bg transparent-img-bg sidebar-border-color sidebar-folder-selected-bg main-color border-color app-bg-secondary conversation-list-bg-opacity group-icon-bg base-secondary-text); .setVariables(group-video-bg group-video-tile-bg participant-audio-connecting-color inactive-call-button-bg inactive-call-button-border inactive-call-button-hover-bg inactive-call-button-hover-border disabled-call-button-bg disabled-call-button-border disabled-call-button-svg button-group-left-hover button-group-right-hover toggle-button-unselected-hover-border toggle-button-unselected-hover-bg); .setVariables(modal-bg modal-border-color); .setVariables(preference-account-input-bg); diff --git a/apps/webapp/src/style/foundation/themes.less b/apps/webapp/src/style/foundation/themes.less index b33f148e727..5e3ef1059eb 100644 --- a/apps/webapp/src/style/foundation/themes.less +++ b/apps/webapp/src/style/foundation/themes.less @@ -39,6 +39,7 @@ body.theme-default { @input-bar-bg: var(--white); @transparent-img-bg: var(--gray-20); @group-icon-bg: var(--white); + @base-secondary-text: var(--gray-70); // ---------------------------------------------------------------------------- // Calling @@ -143,6 +144,7 @@ body.theme-dark { @input-bar-bg: var(--gray-100); @transparent-img-bg: var(--gray-90); @group-icon-bg: var(--gray-95); + @base-secondary-text: var(--gray-60); // ---------------------------------------------------------------------------- // Calling diff --git a/apps/webapp/src/types/i18n.d.ts b/apps/webapp/src/types/i18n.d.ts index dadd22c8ae7..591f7daa183 100644 --- a/apps/webapp/src/types/i18n.d.ts +++ b/apps/webapp/src/types/i18n.d.ts @@ -504,6 +504,10 @@ declare module 'I18n/en-US.json' { 'cells.shareModal.heading': `Share via link`; 'cells.shareModal.linkCopied': `Link copied`; 'cells.shareModal.primaryAction': `Done`; + 'cells.shareModal.password': `Password`; + 'cells.shareModal.password.description': `Set a password to restrict access.`; + 'cells.shareModal.expiration': `Expiration`; + 'cells.shareModal.expiration.description': `Link expires on a selected day.`; 'cells.sidebar.heading': `Files`; 'cells.sidebar.title': `All`; 'cells.tableRow.actions': `More options`; From ddedc762b97e179c132226d6626f29fedb7b3beb Mon Sep 17 00:00:00 2001 From: Arjita Mitra Date: Tue, 13 Jan 2026 11:17:12 +0100 Subject: [PATCH 2/2] feat: create shared password generate section with independent state handling(WPB-20913) --- .../ShareModal/CellsShareModalContent.tsx | 120 +++++++++++++++--- .../ShareModal/CellsSharePasswordStyles.ts | 100 +++++++++++++++ .../CellsShareModal/CellsShareModal.styles.ts | 9 ++ .../CellsShareModal/CellsShareModal.tsx | 29 +++-- .../CellsNodeShareModal.styles.ts | 9 ++ .../CellsNodeShareModal.tsx | 29 +++-- 6 files changed, 253 insertions(+), 43 deletions(-) create mode 100644 apps/webapp/src/script/components/Cells/ShareModal/CellsSharePasswordStyles.ts diff --git a/apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx b/apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx index e114dfc4da6..cb2c944d579 100644 --- a/apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx +++ b/apps/webapp/src/script/components/Cells/ShareModal/CellsShareModalContent.tsx @@ -19,10 +19,14 @@ import {ComponentProps} from 'react'; import {CSSObject} from '@emotion/react'; +import {ValidationUtil} from '@wireapp/commons'; import {BASE_DARK_COLOR, BASE_LIGHT_COLOR, COLOR_V2, Input, Label, Switch} from '@wireapp/react-ui-kit'; import {CellsTableLoader} from 'Components/Conversation/ConversationCells/common/CellsTableLoader/CellsTableLoader'; import {CopyToClipboardButton} from 'Components/CopyToClipboardButton/CopyToClipboardButton'; +import {PasswordGeneratorButton} from 'Components/PasswordGeneratorButton'; +import {Config} from 'src/script/Config'; +import {t} from 'Util/LocalizerUtil'; type PublicLinkStatus = 'idle' | 'loading' | 'error' | 'success'; @@ -49,22 +53,36 @@ interface CellsShareModalContentStyles { switchWrapperStyles: CSSObject; inputStyles: CSSObject; inputWrapperStyles: CSSObject; + passwordContentStyles: CSSObject; + passwordInputRowStyles: CSSObject; + passwordInputLabelStyles: CSSObject; + passwordInputStyles: CSSObject; + passwordActionButtonStyles: CSSObject; + passwordCopyButtonStyles: CSSObject; loaderWrapperStyles: CSSObject; } +interface CellsShareModalContentLabels { + enablePublicLink: string; + password: string; + passwordDescription: string; + expiration: string; + expirationDescription: string; + generatedPublicLink: string; + copyLink: string; + linkCopied: string; + errorLoadingLink: string; + passwordInputLabel: string; + passwordInputPlaceholder: string; + passwordCopy: string; + passwordCopied: string; + showTogglePasswordLabel: string; + hideTogglePasswordLabel: string; +} + interface CellsShareModalContentProps { publicLinkDescription: string; - labels: { - enablePublicLink: string; - password: string; - passwordDescription: string; - expiration: string; - expirationDescription: string; - generatedPublicLink: string; - copyLink: string; - linkCopied: string; - errorLoadingLink: string; - }; + labels?: Partial; publicLink: { status: PublicLinkStatus; link?: string; @@ -75,6 +93,9 @@ interface CellsShareModalContentProps { password: { isEnabled: boolean; onToggle: () => void; + value: string; + onChange: (value: string) => void; + onGeneratePassword: (password: string) => void; }; expiration: { isEnabled: boolean; @@ -98,6 +119,24 @@ const DEFAULT_SWITCH_COLORS: SwitchColorProps = { disabledColorDark: COLOR_V2.GRAY_60, }; +const DEFAULT_LABELS: CellsShareModalContentLabels = { + enablePublicLink: t('cells.shareModal.enablePublicLink'), + password: t('cells.shareModal.password'), + passwordDescription: t('cells.shareModal.password.description'), + expiration: t('cells.shareModal.expiration'), + expirationDescription: t('cells.shareModal.expiration.description'), + generatedPublicLink: t('cells.shareModal.generatedPublicLink'), + copyLink: t('cells.shareModal.copyLink'), + linkCopied: t('cells.shareModal.linkCopied'), + errorLoadingLink: t('cells.shareModal.error.loadingLink'), + passwordInputLabel: t('modalGuestLinkJoinLabel'), + passwordInputPlaceholder: t('modalGuestLinkJoinPlaceholder'), + passwordCopy: t('conversationContextMenuCopy'), + passwordCopied: t('guestOptionsPasswordCopyToClipboardSuccess'), + showTogglePasswordLabel: t('showTogglePasswordLabel'), + hideTogglePasswordLabel: t('hideTogglePasswordLabel'), +}; + export const CellsShareModalContent = ({ publicLinkDescription, labels, @@ -108,6 +147,7 @@ export const CellsShareModalContent = ({ styles, switchColors, }: CellsShareModalContentProps) => { + const resolvedLabels = {...DEFAULT_LABELS, ...labels}; const shouldShowLink = publicLink.isEnabled && publicLink.status === 'success' && publicLink.link; const publicLinkColors = switchColors?.publicLink ?? DEFAULT_SWITCH_COLORS; const passwordColors = switchColors?.password ?? DEFAULT_SWITCH_COLORS; @@ -118,7 +158,7 @@ export const CellsShareModalContent = ({

- {labels.passwordDescription} + {resolvedLabels.passwordDescription}

@@ -155,14 +195,52 @@ export const CellsShareModalContent = ({ />
- {password.isEnabled &&
} + {password.isEnabled && ( +
+
+
+ +
+
+ + password.onChange(event.currentTarget.value)} + pattern={ValidationUtil.getNewPasswordPattern(Config.getConfig().NEW_PASSWORD_MINIMUM_LENGTH)} + wrapperCSS={styles.passwordInputStyles} + /> +
+ +
+
+
+
+ )}

- {labels.expirationDescription} + {resolvedLabels.expirationDescription}

@@ -179,7 +257,7 @@ export const CellsShareModalContent = ({ {shouldShowLink && (
)} @@ -200,7 +278,7 @@ export const CellsShareModalContent = ({
)} - {publicLink.status === 'error' &&
{labels.errorLoadingLink}
} + {publicLink.status === 'error' &&
{resolvedLabels.errorLoadingLink}
}
); }; diff --git a/apps/webapp/src/script/components/Cells/ShareModal/CellsSharePasswordStyles.ts b/apps/webapp/src/script/components/Cells/ShareModal/CellsSharePasswordStyles.ts new file mode 100644 index 00000000000..ce7a62e7607 --- /dev/null +++ b/apps/webapp/src/script/components/Cells/ShareModal/CellsSharePasswordStyles.ts @@ -0,0 +1,100 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + * + */ + +import {CSSObject} from '@emotion/react'; + +export const passwordContentStyles: CSSObject = { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: '12px', +}; + +export const passwordInputRowStyles: CSSObject = { + display: 'grid', + gridTemplateColumns: 'minmax(0, 1fr) auto', + gridTemplateRows: 'auto auto', + alignItems: 'center', + columnGap: '12px', + rowGap: '8px', + width: '100%', +}; + +export const passwordInputLabelStyles: CSSObject = { + fontSize: 'var(--font-size-medium)', + fontWeight: 'var(--font-weight-regular)', + color: 'var(--main-color)', + gridColumn: 1, + gridRow: 1, +}; + +export const passwordInputStyles: CSSObject = { + marginBottom: '0', + flex: 1, + minWidth: 0, + gridColumn: 1, + gridRow: 2, +}; + +export const passwordActionButtonStyles: CSSObject = { + alignSelf: 'flex-start', + '& button': { + backgroundColor: 'var(--white)', + marginBottom: 0, + whiteSpace: 'nowrap', + overflow: 'visible', + textOverflow: 'clip', + }, + 'body.theme-dark & button': { + backgroundColor: 'var(--gray-90)', + }, +}; + +export const passwordCopyButtonStyles: CSSObject = { + alignSelf: 'center', + gridColumn: 2, + gridRow: 2, + '& button': { + backgroundColor: 'var(--white)', + borderRadius: '12px', + border: '1px solid var(--gray-40)', + color: 'var(--main-color)', + cursor: 'pointer', + marginBottom: 0, + whiteSpace: 'nowrap', + overflow: 'visible', + textOverflow: 'clip', + '& svg path': { + fill: 'currentColor', + }, + '&:hover, &:focus': { + backgroundColor: 'var(--gray-20)', + borderColor: 'var(--gray-50)', + }, + }, + 'body.theme-dark & button': { + backgroundColor: 'var(--gray-90)', + border: '1px solid var(--gray-100)', + color: 'var(--white)', + '&:hover, &:focus': { + backgroundColor: 'var(--gray-80)', + borderColor: 'var(--gray-70)', + }, + }, +}; diff --git a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts index a039ee48511..36ac26ece46 100644 --- a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts +++ b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.styles.ts @@ -86,6 +86,15 @@ export const inputWrapperStyles: CSSObject = { gap: '8px', }; +export { + passwordActionButtonStyles, + passwordContentStyles, + passwordCopyButtonStyles, + passwordInputLabelStyles, + passwordInputRowStyles, + passwordInputStyles, +} from 'Components/Cells/ShareModal/CellsSharePasswordStyles'; + export const loaderWrapperStyles: CSSObject = { display: 'flex', justifyContent: 'center', diff --git a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx index 87e45cef1ce..1e6d16c9fbd 100644 --- a/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx +++ b/apps/webapp/src/script/components/CellsGlobalView/CellsTable/CellsTableColumns/CellsShareModal/CellsShareModal.tsx @@ -17,6 +17,8 @@ * */ +import {useState} from 'react'; + import {CellsShareModalContent} from 'Components/Cells/ShareModal/CellsShareModalContent'; import {useCellExpirationToggle} from 'Components/Cells/ShareModal/useCellExpirationToggle'; import {useCellPasswordToggle} from 'Components/Cells/ShareModal/useCellPasswordToggle'; @@ -27,6 +29,12 @@ import {t} from 'Util/LocalizerUtil'; import { inputStyles, inputWrapperStyles, + passwordActionButtonStyles, + passwordContentStyles, + passwordCopyButtonStyles, + passwordInputLabelStyles, + passwordInputRowStyles, + passwordInputStyles, labelStyles, loaderWrapperStyles, dividerStyles, @@ -62,6 +70,7 @@ const CellsShareModal = ({type, uuid, cellsRepository}: ShareModalParams) => { const {status, link, isEnabled, togglePublicLink} = useCellPublicLink({uuid, cellsRepository}); const {isEnabled: isPasswordEnabled, toggle: togglePassword} = useCellPasswordToggle(); const {isEnabled: isExpirationEnabled, toggle: toggleExpiration} = useCellExpirationToggle(); + const [passwordValue, setPasswordValue] = useState(''); const isInputDisabled = ['loading', 'error'].includes(status); @@ -72,17 +81,6 @@ const CellsShareModal = ({type, uuid, cellsRepository}: ShareModalParams) => { ? 'cells.shareModal.enablePublicLink.file.description' : 'cells.shareModal.enablePublicLink.folder.description', )} - labels={{ - enablePublicLink: t('cells.shareModal.enablePublicLink'), - password: t('cells.shareModal.password'), - passwordDescription: t('cells.shareModal.password.description'), - expiration: t('cells.shareModal.expiration'), - expirationDescription: t('cells.shareModal.expiration.description'), - generatedPublicLink: t('cells.shareModal.generatedPublicLink'), - copyLink: t('cells.shareModal.copyLink'), - linkCopied: t('cells.shareModal.linkCopied'), - errorLoadingLink: t('cells.shareModal.error.loadingLink'), - }} publicLink={{ status, link, @@ -93,6 +91,9 @@ const CellsShareModal = ({type, uuid, cellsRepository}: ShareModalParams) => { password={{ isEnabled: isPasswordEnabled, onToggle: togglePassword, + value: passwordValue, + onChange: setPasswordValue, + onGeneratePassword: setPasswordValue, }} expiration={{ isEnabled: isExpirationEnabled, @@ -112,6 +113,12 @@ const CellsShareModal = ({type, uuid, cellsRepository}: ShareModalParams) => { switchWrapperStyles, inputStyles, inputWrapperStyles, + passwordContentStyles, + passwordInputRowStyles, + passwordInputLabelStyles, + passwordInputStyles, + passwordActionButtonStyles, + passwordCopyButtonStyles, loaderWrapperStyles, }} /> diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts index 27bc8da2d7a..0134b0eebcb 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.styles.ts @@ -86,6 +86,15 @@ export const inputWrapperStyles: CSSObject = { gap: '8px', }; +export { + passwordActionButtonStyles, + passwordContentStyles, + passwordCopyButtonStyles, + passwordInputLabelStyles, + passwordInputRowStyles, + passwordInputStyles, +} from 'Components/Cells/ShareModal/CellsSharePasswordStyles'; + export const loaderWrapperStyles: CSSObject = { display: 'flex', justifyContent: 'center', diff --git a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx index b7d9f442aa9..66d2d0fabd7 100644 --- a/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx +++ b/apps/webapp/src/script/components/Conversation/ConversationCells/CellsTable/CellsTableColumns/CellsNodeShareModal/CellsNodeShareModal.tsx @@ -17,6 +17,8 @@ * */ +import {useState} from 'react'; + import {CellsShareModalContent} from 'Components/Cells/ShareModal/CellsShareModalContent'; import {useCellExpirationToggle} from 'Components/Cells/ShareModal/useCellExpirationToggle'; import {useCellPasswordToggle} from 'Components/Cells/ShareModal/useCellPasswordToggle'; @@ -27,6 +29,12 @@ import {t} from 'Util/LocalizerUtil'; import { inputStyles, inputWrapperStyles, + passwordContentStyles, + passwordInputRowStyles, + passwordInputLabelStyles, + passwordInputStyles, + passwordActionButtonStyles, + passwordCopyButtonStyles, labelStyles, loaderWrapperStyles, dividerStyles, @@ -70,6 +78,7 @@ const CellShareModalContent = ({type, uuid, conversationId, cellsRepository}: Sh const {status, link, isEnabled, togglePublicLink} = useCellPublicLink({uuid, conversationId, cellsRepository}); const {isEnabled: isPasswordEnabled, toggle: togglePassword} = useCellPasswordToggle(); const {isEnabled: isExpirationEnabled, toggle: toggleExpiration} = useCellExpirationToggle(); + const [passwordValue, setPasswordValue] = useState(''); const isInputDisabled = ['loading', 'error'].includes(status); @@ -80,17 +89,6 @@ const CellShareModalContent = ({type, uuid, conversationId, cellsRepository}: Sh ? 'cells.shareModal.enablePublicLink.file.description' : 'cells.shareModal.enablePublicLink.folder.description', )} - labels={{ - enablePublicLink: t('cells.shareModal.enablePublicLink'), - password: t('cells.shareModal.password'), - passwordDescription: t('cells.shareModal.password.description'), - expiration: t('cells.shareModal.expiration'), - expirationDescription: t('cells.shareModal.expiration.description'), - generatedPublicLink: t('cells.shareModal.generatedPublicLink'), - copyLink: t('cells.shareModal.copyLink'), - linkCopied: t('cells.shareModal.linkCopied'), - errorLoadingLink: t('cells.shareModal.error.loadingLink'), - }} publicLink={{ status, link, @@ -101,6 +99,9 @@ const CellShareModalContent = ({type, uuid, conversationId, cellsRepository}: Sh password={{ isEnabled: isPasswordEnabled, onToggle: togglePassword, + value: passwordValue, + onChange: setPasswordValue, + onGeneratePassword: setPasswordValue, }} expiration={{ isEnabled: isExpirationEnabled, @@ -120,6 +121,12 @@ const CellShareModalContent = ({type, uuid, conversationId, cellsRepository}: Sh switchWrapperStyles, inputStyles, inputWrapperStyles, + passwordContentStyles, + passwordInputRowStyles, + passwordInputLabelStyles, + passwordInputStyles, + passwordActionButtonStyles, + passwordCopyButtonStyles, loaderWrapperStyles, }} />