From 3591c90f9fb8b7ac64e4b8929d4ab659c63c819e Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Sun, 24 Oct 2021 22:46:39 +0200 Subject: [PATCH] Fix file input field accepting a filename only once (#1547) --- cypress/fixtures/history-2.json | 1 + cypress/fixtures/history-2.json.license | 3 + cypress/fixtures/history.json | 1 + cypress/fixtures/history.json.license | 3 + cypress/integration/history.spec.ts | 52 +++++++++++- src/api/history/index.ts | 4 +- .../history-page/entry-menu/entry-menu.tsx | 3 +- .../entry-menu/remove-note-entry-item.tsx | 2 + .../history-toolbar/clear-history-button.tsx | 7 +- .../history-toolbar/import-history-button.tsx | 85 ++++++++++++------- .../notifications/ui-notification-toast.tsx | 6 +- 11 files changed, 130 insertions(+), 37 deletions(-) create mode 100644 cypress/fixtures/history-2.json create mode 100644 cypress/fixtures/history-2.json.license create mode 100644 cypress/fixtures/history.json create mode 100644 cypress/fixtures/history.json.license diff --git a/cypress/fixtures/history-2.json b/cypress/fixtures/history-2.json new file mode 100644 index 000000000..0afdc98ff --- /dev/null +++ b/cypress/fixtures/history-2.json @@ -0,0 +1 @@ +{"version":2,"entries":[{"identifier":"cypress2","title":"cy-Test2","tags":[],"lastVisited":"2019-04-30T09:36:45.249+02:00","pinStatus":false}]} diff --git a/cypress/fixtures/history-2.json.license b/cypress/fixtures/history-2.json.license new file mode 100644 index 000000000..078e5a9ac --- /dev/null +++ b/cypress/fixtures/history-2.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 diff --git a/cypress/fixtures/history.json b/cypress/fixtures/history.json new file mode 100644 index 000000000..98abb1133 --- /dev/null +++ b/cypress/fixtures/history.json @@ -0,0 +1 @@ +{"version":2,"entries":[{"identifier":"cypress","title":"cy-Test","tags":[],"lastVisited":"2019-04-30T09:36:45.249+02:00","pinStatus":false}]} diff --git a/cypress/fixtures/history.json.license b/cypress/fixtures/history.json.license new file mode 100644 index 000000000..078e5a9ac --- /dev/null +++ b/cypress/fixtures/history.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + +SPDX-License-Identifier: CC0-1.0 diff --git a/cypress/integration/history.spec.ts b/cypress/integration/history.spec.ts index 8dc00cc0e..34b1d2668 100644 --- a/cypress/integration/history.spec.ts +++ b/cypress/integration/history.spec.ts @@ -5,7 +5,6 @@ */ describe('History', () => { - describe('History Mode', () => { beforeEach(() => { cy.visit('/history') @@ -125,4 +124,55 @@ describe('History', () => { }) }) }) + + describe('Import', () => { + beforeEach(() => { + cy.clearLocalStorage('history') + cy.intercept('GET', '/mock-backend/api/private/me/history', { + body: [] + }) + cy.visit('/history') + cy.logout() + }) + + it('works with valid file', () => { + cy.get('[data-cypress-id="import-history-file-button"]').click() + cy.get('[data-cypress-id="import-history-file-input"]').attachFile({ + filePath: 'history.json', + mimeType: 'application/json' + }) + cy.get('[data-cypress-id="history-entry-title"]') + .should('have.length', 1) + .contains('cy-Test') + }) + + it('fails on invalid file', () => { + cy.get('[data-cypress-id="import-history-file-button"]').click() + cy.get('[data-cypress-id="import-history-file-input"]').attachFile({ + filePath: 'history.json.license', + mimeType: 'text/plain' + }) + cy.get('[data-cypress-id="notification-toast"]').should('be.visible') + }) + + it('works when selecting two files with the same name', () => { + cy.get('[data-cypress-id="import-history-file-button"]').click() + cy.get('[data-cypress-id="import-history-file-input"]').attachFile({ + filePath: 'history.json', + mimeType: 'application/json' + }) + cy.get('[data-cypress-id="history-entry-title"]') + .should('have.length', 1) + .contains('cy-Test') + cy.get('[data-cypress-id="import-history-file-button"]').click() + cy.get('[data-cypress-id="import-history-file-input"]').attachFile({ + filePath: 'history-2.json', + fileName: 'history.json', + mimeType: 'application/json' + }) + cy.get('[data-cypress-id="history-entry-title"]') + .should('have.length', 2) + .contains('cy-Test2') + }) + }) }) diff --git a/src/api/history/index.ts b/src/api/history/index.ts index c1fe26860..126c7bfec 100644 --- a/src/api/history/index.ts +++ b/src/api/history/index.ts @@ -8,7 +8,9 @@ import { defaultFetchConfig, expectResponseCode, getApiUrl } from '../utils' import type { HistoryEntryDto, HistoryEntryPutDto, HistoryEntryUpdateDto } from './types' export const getHistory = async (): Promise => { - const response = await fetch(getApiUrl() + 'me/history') + const response = await fetch(getApiUrl() + 'me/history', { + ...defaultFetchConfig + }) expectResponseCode(response) return (await response.json()) as Promise } diff --git a/src/components/history-page/entry-menu/entry-menu.tsx b/src/components/history-page/entry-menu/entry-menu.tsx index 7f28ff498..8c6901826 100644 --- a/src/components/history-page/entry-menu/entry-menu.tsx +++ b/src/components/history-page/entry-menu/entry-menu.tsx @@ -14,6 +14,7 @@ import './entry-menu.scss' import { RemoveNoteEntryItem } from './remove-note-entry-item' import { HistoryEntryOrigin } from '../../../redux/history/types' import { useApplicationState } from '../../../hooks/common/use-application-state' +import { cypressId } from '../../../utils/cypress-attribute' export interface EntryMenuProps { id: string @@ -31,7 +32,7 @@ export const EntryMenu: React.FC = ({ id, title, origin, isDark, const userExists = useApplicationState((state) => !!state.user) return ( - + void @@ -23,6 +24,7 @@ export const RemoveNoteEntryItem: React.FC = ({ noteTi modalQuestionI18nKey={'landing.history.modal.removeNote.question'} modalWarningI18nKey={'landing.history.modal.removeNote.warning'} noteTitle={noteTitle} + {...cypressId('history-entry-menu-remove-button')} /> ) } diff --git a/src/components/history-page/history-toolbar/clear-history-button.tsx b/src/components/history-page/history-toolbar/clear-history-button.tsx index 6235938bc..f0eddcda9 100644 --- a/src/components/history-page/history-toolbar/clear-history-button.tsx +++ b/src/components/history-page/history-toolbar/clear-history-button.tsx @@ -11,6 +11,7 @@ import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon' import { DeletionModal } from '../../common/modals/deletion-modal' import { deleteAllHistoryEntries, refreshHistoryState } from '../../../redux/history/methods' import { showErrorNotification } from '../../../redux/ui-notifications/methods' +import { cypressId } from '../../../utils/cypress-attribute' export const ClearHistoryButton: React.FC = () => { const { t } = useTranslation() @@ -29,7 +30,11 @@ export const ClearHistoryButton: React.FC = () => { return ( - { const { t } = useTranslation() const userExists = useApplicationState((state) => !!state.user) const historyState = useApplicationState((state) => state.history) const uploadInput = useRef(null) - const [show, setShow] = useState(false) const [fileName, setFilename] = useState('') - const [i18nKey, setI18nKey] = useState('') - - const handleShow = useCallback((key: string) => { - setI18nKey(key) - setShow(true) - }, []) - - const handleClose = useCallback(() => { - setI18nKey('') - setShow(false) - }, []) const onImportHistory = useCallback( (entries: HistoryEntry[]): void => { @@ -50,17 +38,29 @@ export const ImportHistoryButton: React.FC = () => { [historyState, userExists] ) - const handleUpload = (event: React.ChangeEvent) => { + const resetInputField = useCallback(() => { + if (!uploadInput.current) { + return + } + uploadInput.current.value = '' + }, [uploadInput]) + + const handleUpload = async (event: React.ChangeEvent) => { const { validity, files } = event.target if (files && files[0] && validity.valid) { const file = files[0] setFilename(file.name) if (file.type !== 'application/json' && file.type !== '') { - handleShow('landing.history.modal.importHistoryError.textWithFile') + await dispatchUiNotification('common.errorOccurred', 'landing.history.modal.importHistoryError.textWithFile', { + contentI18nOptions: { + fileName + } + }) + resetInputField() return } const fileReader = new FileReader() - fileReader.onload = (event) => { + fileReader.onload = async (event) => { if (event.target && event.target.result) { try { const result = event.target.result as string @@ -71,42 +71,63 @@ export const ImportHistoryButton: React.FC = () => { onImportHistory(data.entries) } else { // probably a newer version we can't support - handleShow('landing.history.modal.importHistoryError.tooNewVersion') + await dispatchUiNotification( + 'common.errorOccurred', + 'landing.history.modal.importHistoryError.tooNewVersion', + { + contentI18nOptions: { + fileName + } + } + ) } } else { const oldEntries = JSON.parse(result) as V1HistoryEntry[] onImportHistory(convertV1History(oldEntries)) } } + resetInputField() } catch { - handleShow('landing.history.modal.importHistoryError.textWithFile') + await dispatchUiNotification( + 'common.errorOccurred', + 'landing.history.modal.importHistoryError.textWithFile', + { + contentI18nOptions: { + fileName + } + } + ) } } } fileReader.readAsText(file) } else { - handleShow('landing.history.modal.importHistoryError.textWithOutFile') + await dispatchUiNotification( + 'common.errorOccurred', + 'landing.history.modal.importHistoryError.textWithOutFile', + {} + ) + resetInputField() } } return (
- + - -
- -
-
) } diff --git a/src/components/notifications/ui-notification-toast.tsx b/src/components/notifications/ui-notification-toast.tsx index 009d711ee..761eea1eb 100644 --- a/src/components/notifications/ui-notification-toast.tsx +++ b/src/components/notifications/ui-notification-toast.tsx @@ -13,6 +13,7 @@ import type { IconName } from '../common/fork-awesome/types' import { dismissUiNotification } from '../../redux/ui-notifications/methods' import { Trans, useTranslation } from 'react-i18next' import { Logger } from '../../utils/logger' +import { cypressId } from '../../utils/cypress-attribute' const STEPS_PER_SECOND = 10 const log = new Logger('UiNotificationToast') @@ -108,7 +109,10 @@ export const UiNotificationToast: React.FC = ({ }, [contentI18nKey, contentI18nOptions, t]) return ( - +