Skip to content

Commit

Permalink
refactor: remove password logic form modal implementation
Browse files Browse the repository at this point in the history
Removes the password logic from the modal implementation and uses a custom component instead.
  • Loading branch information
JammingBen committed Dec 15, 2023
1 parent 82ed990 commit 0b88637
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 97 deletions.
31 changes: 1 addition & 30 deletions packages/design-system/src/components/OcModal/OcModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,10 @@
:placeholder="inputPlaceholder"
:label="inputLabel"
:type="inputType"
:password-policy="inputPasswordPolicy"
:generate-password-method="inputGeneratePasswordMethod"
:description-message="inputDescription"
:disabled="inputDisabled"
:fix-message-line="true"
:selection-range="inputSelectionRange"
@password-challenge-completed="$emit('passwordChallengeCompleted')"
@password-challenge-failed="$emit('passwordChallengeFailed')"
@update:model-value="inputOnInput"
@keydown.enter.prevent="confirm"
/>
Expand Down Expand Up @@ -107,7 +103,6 @@ import OcIcon from '../OcIcon/OcIcon.vue'
import OcTextInput from '../OcTextInput/OcTextInput.vue'
import { FocusTrap } from 'focus-trap-vue'
import { FocusTargetOrFalse, FocusTargetValueOrFalse } from 'focus-trap'
import { PasswordPolicy } from '../../helpers'
/**
* Modals are generally used to force the user to focus on confirming or completing a single action.
Expand Down Expand Up @@ -364,22 +359,6 @@ export default defineComponent({
required: false,
default: false
},
/**
* Password policy for the input
*/
inputPasswordPolicy: {
type: Object as PropType<PasswordPolicy>,
required: false,
default: () => ({})
},
/**
* Method to generate random password for the input
*/
inputGeneratePasswordMethod: {
type: Function as PropType<(...args: unknown[]) => string>,
required: false,
default: null
},
/**
* Overwrite default focused element
* Can be `#id, .class`.
Expand All @@ -397,15 +376,7 @@ export default defineComponent({
default: false
}
},
emits: [
'cancel',
'confirm',
'confirm-secondary',
'input',
'checkbox-changed',
'passwordChallengeCompleted',
'passwordChallengeFailed'
],
emits: ['cancel', 'confirm', 'confirm-secondary', 'input', 'checkbox-changed'],
setup() {
const primaryButton = ref(null)
const secondaryButton = ref(null)
Expand Down
106 changes: 106 additions & 0 deletions packages/web-app-files/src/components/Modals/SetLinkPasswordModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<template>
<oc-text-input
:model-value="password"
:label="$gettext('Password')"
type="password"
:password-policy="inputPasswordPolicy"
:generate-password-method="inputGeneratePasswordMethod"
:fix-message-line="true"
:placeholder="link.password ? '●●●●●●●●' : null"
:error-message="errorMessage"
class="oc-modal-body-input"
@password-challenge-completed="confirmDisabled = false"
@password-challenge-failed="confirmDisabled = true"
@keydown.enter.prevent="onConfirm"
@update:model-value="onInput"
/>
<div class="oc-flex oc-flex-right oc-flex-middle oc-mt-m">
<oc-button
class="oc-modal-body-actions-cancel oc-ml-s"
appearance="outline"
variation="passive"
@click="onCancel"
>{{ $gettext('Cancel') }}
</oc-button>
<oc-button
class="oc-modal-body-actions-confirm oc-ml-s"
appearance="filled"
variation="primary"
:disabled="confirmDisabled"
@click="onConfirm"
>{{ link.password ? $gettext('Apply') : $gettext('Set') }}
</oc-button>
</div>
</template>

<script lang="ts">
import { defineComponent, ref, unref, PropType } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useClientService, usePasswordPolicyService, useStore } from '@ownclouders/web-pkg'
import { Share } from '@ownclouders/web-client/src/helpers'
export default defineComponent({
name: 'SetLinkPasswordModal',
props: {
link: { type: Object as PropType<Share>, required: true }
},
setup(props, { expose }) {
const store = useStore()
const clientService = useClientService()
const passwordPolicyService = usePasswordPolicyService()
const { $gettext } = useGettext()
const password = ref('')
const errorMessage = ref<string>()
const confirmDisabled = ref(true)
const onInput = (value: string) => {
password.value = value
errorMessage.value = undefined
}
const onConfirm = async () => {
try {
await store.dispatch('Files/updateLink', {
id: props.link.id,
client: clientService.owncloudSdk,
params: { ...props.link, password: unref(password) }
})
store.dispatch('hideModal')
store.dispatch('showMessage', {
title: $gettext('Link was updated successfully')
})
} catch (e) {
// Human-readable error message is provided, for example when password is on banned list
if (e.statusCode === 400) {
errorMessage.value = $gettext(e.message)
return
}
store.dispatch('showErrorMessage', {
title: $gettext('Failed to update link'),
error: e
})
}
}
const onCancel = () => {
store.dispatch('hideModal')
}
expose({ onConfirm, onCancel })
return {
password,
confirmDisabled,
onInput,
onConfirm,
onCancel,
errorMessage,
passwordPolicyService,
inputPasswordPolicy: passwordPolicyService.getPolicy(),
inputGeneratePasswordMethod: () => passwordPolicyService.generatePassword()
}
}
})
</script>
33 changes: 9 additions & 24 deletions packages/web-app-files/src/components/SideBar/Shares/FileLinks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ import {
useCapabilityFilesSharingPublicAlias,
useCapabilityFilesSharingPublicPasswordEnforcedFor,
useAbility,
usePasswordPolicyService,
useExpirationRules,
useDefaultLinkPermissions,
useFileActionsCreateLink,
Expand All @@ -143,6 +142,7 @@ import {
import DetailsAndEdit from './Links/DetailsAndEdit.vue'
import NameAndCopy from './Links/NameAndCopy.vue'
import CreateQuickLink from './Links/CreateQuickLink.vue'
import SetLinkPasswordModal from '../../Modals/SetLinkPasswordModal.vue'
import { getLocaleFromLanguage } from '@ownclouders/web-pkg'
import {
Resource,
Expand All @@ -169,7 +169,6 @@ export default defineComponent({
const clientService = useClientService()
const { can } = ability
const { expirationRules } = useExpirationRules()
const passwordPolicyService = usePasswordPolicyService()
const hasResharing = useCapabilityFilesSharingResharing()
const hasShareJail = useCapabilityShareJailEnabled()
const { defaultLinkPermissions } = useDefaultLinkPermissions()
Expand Down Expand Up @@ -269,28 +268,14 @@ export default defineComponent({
}
}
const showQuickLinkPasswordModal = (params) => {
const modal = {
const showPasswordModal = (params) => {
return store.dispatch('createModal', {
variation: 'passive',
title: $gettext('Set password'),
cancelText: $gettext('Cancel'),
confirmText: $gettext('Set'),
hasInput: true,
inputDescription: $gettext('Passwords for links are required.'),
inputPasswordPolicy: passwordPolicyService.getPolicy(),
inputGeneratePasswordMethod: () => passwordPolicyService.generatePassword(),
inputLabel: $gettext('Password'),
inputType: 'password',
onInput: () => store.dispatch('setModalInputErrorMessage', ''),
onPasswordChallengeCompleted: () => store.dispatch('setModalConfirmButtonDisabled', false),
onPasswordChallengeFailed: () => store.dispatch('setModalConfirmButtonDisabled', true),
onCancel: () => store.dispatch('hideModal'),
onConfirm: (newPassword: string) => {
return updatePublicLink({ params: { ...params, password: newPassword } })
}
}
return store.dispatch('createModal', modal)
hideActions: true,
customComponent: SetLinkPasswordModal,
customComponentAttrs: { link: params }
})
}
return {
Expand All @@ -316,7 +301,7 @@ export default defineComponent({
canEditLink,
expirationRules,
updatePublicLink,
showQuickLinkPasswordModal,
showPasswordModal,
defaultLinkPermissions,
addNewLink
}
Expand Down Expand Up @@ -465,7 +450,7 @@ export default defineComponent({
}
if (!link.password && !this.canDeletePublicLinkPassword(link)) {
this.showQuickLinkPasswordModal(params)
this.showPasswordModal(params)
} else {
this.updatePublicLink({ params })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ import * as EmailValidator from 'email-validator'
import {
createLocationSpaces,
useConfigurationManager,
LinkRoleDropdown
LinkRoleDropdown,
useStore
} from '@ownclouders/web-pkg'
import {
linkRoleInternalFile,
Expand All @@ -170,6 +171,7 @@ import { createFileRouteOptions } from '@ownclouders/web-pkg'
import { OcDrop } from 'design-system/src/components'
import { usePasswordPolicyService, ExpirationRules } from '@ownclouders/web-pkg'
import { useGettext } from 'vue3-gettext'
import SetLinkPasswordModal from '../../../Modals/SetLinkPasswordModal.vue'
export default defineComponent({
name: 'DetailsAndEdit',
Expand Down Expand Up @@ -210,7 +212,8 @@ export default defineComponent({
},
emits: ['removePublicLink', 'updateLink'],
setup(props, { emit }) {
const { current } = useGettext()
const store = useStore()
const { $gettext, current } = useGettext()
const configurationManager = useConfigurationManager()
const passwordPolicyService = usePasswordPolicyService()
Expand All @@ -236,6 +239,16 @@ export default defineComponent({
})
}
const showPasswordModal = () => {
return store.dispatch('createModal', {
variation: 'passive',
title: props.link.password ? $gettext('Edit password') : $gettext('Add password'),
hideActions: true,
customComponent: SetLinkPasswordModal,
customComponentAttrs: { link: props.link }
})
}
return {
space: inject<Ref<SpaceResource>>('space'),
resource: inject<Ref<Resource>>('resource'),
Expand All @@ -244,7 +257,8 @@ export default defineComponent({
updateLink,
updateSelectedRole,
currentLinkRole,
isRunningOnEos: computed(() => configurationManager.options.isRunningOnEos)
isRunningOnEos: computed(() => configurationManager.options.isRunningOnEos),
showPasswordModal
}
},
data() {
Expand Down Expand Up @@ -503,38 +517,6 @@ export default defineComponent({
this.createModal(modal)
},
showPasswordModal() {
const modal = {
variation: 'passive',
title: this.link.password ? this.$gettext('Edit password') : this.$gettext('Add password'),
cancelText: this.$gettext('Cancel'),
confirmText: this.link.password ? this.$gettext('Apply') : this.$gettext('Set'),
hasInput: true,
confirmDisabled: true,
inputLabel: this.$gettext('Password'),
inputPasswordPolicy: this.passwordPolicyService.getPolicy(),
inputGeneratePasswordMethod: () => this.passwordPolicyService.generatePassword(),
inputPlaceholder: this.link.password ? '●●●●●●●●' : null,
inputType: 'password',
onCancel: this.hideModal,
onInput: () => this.setModalInputErrorMessage(''),
onPasswordChallengeCompleted: () => this.setModalConfirmButtonDisabled(false),
onPasswordChallengeFailed: () => this.setModalConfirmButtonDisabled(true),
onConfirm: (password) => {
this.updateLink({
link: {
...this.link,
password
},
onSuccess: () => {
this.hideModal()
}
})
}
}
this.createModal(modal)
},
toggleNotifyUploads() {
if (this.currentLinkNotifyUploads) {
this.$emit('updateLink', {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import SetLinkPasswordModal from '../../../../src/components/Modals/SetLinkPasswordModal.vue'
import {
createStore,
defaultComponentMocks,
defaultPlugins,
defaultStoreMockOptions,
shallowMount
} from 'web-test-helpers'

describe('SetLinkPasswordModal', () => {
it('should render a text input field for the password', () => {
const { wrapper } = getWrapper()

expect(wrapper.find('oc-text-input-stub').exists()).toBeTruthy()
})
describe('method "onConfirm"', () => {
it('updates the link', async () => {
const { wrapper, storeOptions } = getWrapper()
await wrapper.vm.onConfirm()

expect(storeOptions.modules.Files.actions.updateLink).toHaveBeenCalled()
expect(storeOptions.actions.showMessage).toHaveBeenCalled()
})
it('shows an error message on error', async () => {
const { wrapper, storeOptions } = getWrapper()
storeOptions.modules.Files.actions.updateLink.mockRejectedValue(new Error(''))
await wrapper.vm.onConfirm()

expect(storeOptions.actions.showErrorMessage).toHaveBeenCalled()
})
})
})

function getWrapper({ link = {} } = {}) {
const mocks = { ...defaultComponentMocks() }

const storeOptions = defaultStoreMockOptions
const store = createStore(storeOptions)

return {
mocks,
storeOptions,
wrapper: shallowMount(SetLinkPasswordModal, {
props: {
link
},
global: {
plugins: [...defaultPlugins(), store],
mocks,
provide: mocks
}
})
}
}
Loading

0 comments on commit 0b88637

Please sign in to comment.