From 1bd6de3439b5021c65255ccc5364bd22cdd28736 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Mon, 12 May 2025 14:27:57 +0200 Subject: [PATCH] Refactor Template Gallery; resolves #38 and #39 - Replace free-text license input with a select box - Improve visual presentation of modals and enhance keyboard interaction --- .../Features/Project/ProjectController.mjs | 5 - .../Features/Templates/TemplatesManager.mjs | 2 +- .../web/frontend/extracted-translations.json | 3 +- .../components/actions-manage-template.tsx | 2 +- .../editor-manage-template-modal-wrapper.jsx | 37 ---- .../manage-template-modal-content.jsx | 203 ------------------ .../components/settings-template-category.tsx | 51 ----- .../components/gallery-search-sort-header.tsx | 2 +- .../components/search-form.tsx | 2 + .../components/delete-template-button.tsx | 6 +- .../components/edit-template-button.tsx | 4 +- .../components/edit-template-modal.tsx | 153 ------------- .../components/form/form-field-input.tsx | 17 ++ .../form/labeled-row-form-group.tsx | 26 +++ .../components/form/template-form-fields.tsx | 96 +++++++++ .../editor-manage-template-modal-wrapper.tsx | 40 ++++ .../manage-template-modal-content.tsx | 169 +++++++++++++++ .../manage-template-modal.tsx} | 21 +- .../{ => modals}/delete-template-modal.tsx | 2 +- .../components/modals/edit-template-modal.tsx | 139 ++++++++++++ .../{ => modals}/template-action-modal.tsx | 93 ++++---- .../components/settings/settings-language.tsx | 11 +- .../components/settings/settings-license.tsx | 33 +++ .../settings/settings-menu-select.tsx | 49 ++--- .../settings/settings-template-category.tsx | 36 ++-- .../template/components/template-details.tsx | 3 +- .../features/template/hooks/use-focus-trap.ts | 46 ++++ .../frontend/js/pages/template-gallery.tsx | 5 +- services/web/frontend/js/pages/template.tsx | 5 +- .../stylesheets/pages/templates-v2.scss | 20 +- services/web/locales/de.json | 3 +- services/web/locales/en.json | 5 +- services/web/locales/ru.json | 3 +- .../app/src/TemplateGalleryRouter.mjs | 13 +- .../app/src/models/Template.js | 2 +- services/web/types/template.ts | 6 +- 36 files changed, 723 insertions(+), 590 deletions(-) delete mode 100644 services/web/frontend/js/features/manage-template-modal/components/editor-manage-template-modal-wrapper.jsx delete mode 100644 services/web/frontend/js/features/manage-template-modal/components/manage-template-modal-content.jsx delete mode 100644 services/web/frontend/js/features/manage-template-modal/components/settings-template-category.tsx delete mode 100644 services/web/frontend/js/features/template/components/edit-template-modal.tsx create mode 100644 services/web/frontend/js/features/template/components/form/form-field-input.tsx create mode 100644 services/web/frontend/js/features/template/components/form/labeled-row-form-group.tsx create mode 100644 services/web/frontend/js/features/template/components/form/template-form-fields.tsx create mode 100644 services/web/frontend/js/features/template/components/manage-template-modal/editor-manage-template-modal-wrapper.tsx create mode 100644 services/web/frontend/js/features/template/components/manage-template-modal/manage-template-modal-content.tsx rename services/web/frontend/js/features/{manage-template-modal/components/manage-template-modal.jsx => template/components/manage-template-modal/manage-template-modal.tsx} (78%) rename services/web/frontend/js/features/template/components/{ => modals}/delete-template-modal.tsx (93%) create mode 100644 services/web/frontend/js/features/template/components/modals/edit-template-modal.tsx rename services/web/frontend/js/features/template/components/{ => modals}/template-action-modal.tsx (60%) create mode 100644 services/web/frontend/js/features/template/components/settings/settings-license.tsx create mode 100644 services/web/frontend/js/features/template/hooks/use-focus-trap.ts diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index d2cf6b1faa..8f342733e0 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/services/web/app/src/Features/Project/ProjectController.mjs @@ -686,16 +686,11 @@ const _ProjectController = { ) } -console.log("Features.hasFeature('templates-server-pro') = ", Features.hasFeature('templates-server-pro')) -console.log("Settings.templates?.nonAdminCanManage", Settings.templates?.nonAdminCanManage) - const isAdminOrTemplateOwner = hasAdminAccess(user) || Settings.templates?.nonAdminCanManage const showTemplatesServerPro = Features.hasFeature('templates-server-pro') && isAdminOrTemplateOwner -console.log("showTemplatesServerPro = ", showTemplatesServerPro ) - const debugPdfDetach = shouldDisplayFeature('debug_pdf_detach') const detachRole = req.params.detachRole diff --git a/services/web/app/src/Features/Templates/TemplatesManager.mjs b/services/web/app/src/Features/Templates/TemplatesManager.mjs index 1008e96311..8a7420a636 100644 --- a/services/web/app/src/Features/Templates/TemplatesManager.mjs +++ b/services/web/app/src/Features/Templates/TemplatesManager.mjs @@ -97,7 +97,7 @@ const TemplatesManager = { await ProjectOptionsHandler.setImageName(projectId, imageName) } catch { logger.warn({ imageName: imageName }, 'not available') - await ProjectOptionsHandler.setImageName(projectId, process.env.TEX_LIVE_DOCKER_IMAGE) + await ProjectOptionsHandler.setImageName(projectId, settings.currentImageName) } }, diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 31d385e406..769e7a42e8 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -232,6 +232,7 @@ "card_payment": "", "careers": "", "categories": "", + "category": "", "category_arrows": "", "category_greek": "", "category_misc": "", @@ -1834,10 +1835,8 @@ "tax_id_type": "", "tell_the_project_owner_and_ask_them_to_upgrade": "", "template": "", - "template_category": "", "template_description": "", "template_gallery": "", - "template_title": "", "template_title_taken_from_project_title": "", "templates": "", "temporarily_hides_the_preview": "", diff --git a/services/web/frontend/js/features/editor-left-menu/components/actions-manage-template.tsx b/services/web/frontend/js/features/editor-left-menu/components/actions-manage-template.tsx index 30d1e81eea..f277111a91 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/actions-manage-template.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/actions-manage-template.tsx @@ -4,7 +4,7 @@ import * as eventTracking from '../../../infrastructure/event-tracking' import getMeta from '../../../utils/meta' import OLTooltip from '@/features/ui/components/ol/ol-tooltip' import { useDetachCompileContext } from '../../../shared/context/detach-compile-context' -import EditorManageTemplateModalWrapper from '../../manage-template-modal/components/editor-manage-template-modal-wrapper' +import EditorManageTemplateModalWrapper from '../../template/components/manage-template-modal/editor-manage-template-modal-wrapper' import LeftMenuButton from './left-menu-button' type TemplateManageResponse = { diff --git a/services/web/frontend/js/features/manage-template-modal/components/editor-manage-template-modal-wrapper.jsx b/services/web/frontend/js/features/manage-template-modal/components/editor-manage-template-modal-wrapper.jsx deleted file mode 100644 index e24cae0821..0000000000 --- a/services/web/frontend/js/features/manage-template-modal/components/editor-manage-template-modal-wrapper.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import withErrorBoundary from '../../../infrastructure/error-boundary' -import { useProjectContext } from '../../../shared/context/project-context' -import ManageTemplateModal from './manage-template-modal' - -const EditorManageTemplateModalWrapper = React.memo( - function EditorManageTemplateModalWrapper({ show, handleHide, openTemplate }) { - const { - _id: projectId, - name: projectName, - } = useProjectContext() - - if (!projectName) { - // wait for useProjectContext - return null - } else { - return ( - - ) - } - } -) - -EditorManageTemplateModalWrapper.propTypes = { - show: PropTypes.bool.isRequired, - handleHide: PropTypes.func.isRequired, - openTemplate: PropTypes.func.isRequired, -} - -export default withErrorBoundary(EditorManageTemplateModalWrapper) diff --git a/services/web/frontend/js/features/manage-template-modal/components/manage-template-modal-content.jsx b/services/web/frontend/js/features/manage-template-modal/components/manage-template-modal-content.jsx deleted file mode 100644 index 54b3da3523..0000000000 --- a/services/web/frontend/js/features/manage-template-modal/components/manage-template-modal-content.jsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useMemo, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import PropTypes from 'prop-types' -import { debugConsole } from '@/utils/debugging' -import { getJSON, postJSON } from '../../../infrastructure/fetch-json' -import Notification from '@/shared/components/notification' -import { - OLModalBody, - OLModalFooter, - OLModalHeader, - OLModalTitle, -} from '@/features/ui/components/ol/ol-modal' -import OLForm from '@/features/ui/components/ol/ol-form' -import OLFormGroup from '@/features/ui/components/ol/ol-form-group' -import OLFormControl from '@/features/ui/components/ol/ol-form-control' -import OLFormLabel from '@/features/ui/components/ol/ol-form-label' -import OLButton from '@/features/ui/components/ol/ol-button' -import { useDetachCompileContext } from '../../../shared/context/detach-compile-context' -import { useUserContext } from '../../../shared/context/user-context' -import SettingsTemplateCategory from './settings-template-category' - -const defaultLicense = 'Creative Commons CC BY 4.0' - -export default function ManageTemplateModalContent({ - handleHide, - inFlight, - setInFlight, - handleAfterPublished, - projectId, - projectName, -}) { - const { t } = useTranslation() - - const { pdfFile } = useDetachCompileContext() - const user = useUserContext() - - const [error, setError] = useState() - const [disablePublish, setDisablePublish] = useState(false) - const [notificationType, setNotificationType] = useState('error') - const [name, setName] = useState(`${projectName}`) - const [description, setDescription] = useState('') - const [author, setAuthor] = useState(`${user.first_name} ${user.last_name}`.trim()) - const [license, setLicense] = useState(defaultLicense) - const [category, setCategory] = useState() - const [override, setOverride] = useState(false) - const [titleConflict, setTitleConflict] = useState(false) - - const valid = useMemo( - () => name.trim().length > 0 && license.trim().length, - [name, license] - ) - - useEffect(() => { - const queryParams = new URLSearchParams({ key: 'name', val: projectName }) - getJSON(`/api/template?${queryParams}`) - .then((data) => { - if (!data) return - setDescription(data.descriptionMD) - setAuthor(data.authorMD) - setLicense(data.license) - setCategory(data.category) - }) - .catch(debugConsole.error) - }, []) - - const handleSubmit = event => { - event.preventDefault() - - if (!valid) { - return - } - - setError(false) - setInFlight(true) - - postJSON(`/template/new/${projectId}`, { - body: { - category, - name: name.trim(), - authorMD: author.trim(), - license: license.trim(), - descriptionMD: description.trim(), - build: pdfFile.build, - override, - }, - }) - .then(data => { - // redirect to template page - handleHide() - handleAfterPublished(data) - }) - .catch(({ response, data }) => { - if (response?.status === 409 && data.canOverride) { - setNotificationType('warning') - setOverride(true) - } else { - setNotificationType('error') - setDisablePublish(true) - } - setError(data.message) - if (response?.status === 409) setTitleConflict(true) - }) - .finally(() => { - setInFlight(false) - }) - } - - const handleNameChange = event => { - setName(event.target.value) - if (titleConflict) { - setError(false) - setOverride(false) - if (disablePublish) setDisablePublish(false) - } - } - - return ( - <> - - {t('publish_as_template')} - - - - - - {t('template_title')} - - - - - - - {t('Author')} - setAuthor(event.target.value)} - /> - - - {t('License')} - setLicense(event.target.value)} - /> - - - {t('template_description')} - setDescription(event.target.value)} - rows={4} - autoFocus - /> - - - {error && ( - - )} - - - - - {t('cancel')} - - - {inFlight ? <>{t('publishing')}… : override ? t('overwrite') : t('publish')} - - - - ) -} - -ManageTemplateModalContent.propTypes = { - handleHide: PropTypes.func.isRequired, - inFlight: PropTypes.bool, - handleAfterPublished: PropTypes.func.isRequired, - setInFlight: PropTypes.func.isRequired, - projectId: PropTypes.string, - projectName: PropTypes.string, -} diff --git a/services/web/frontend/js/features/manage-template-modal/components/settings-template-category.tsx b/services/web/frontend/js/features/manage-template-modal/components/settings-template-category.tsx deleted file mode 100644 index 747fbe73cf..0000000000 --- a/services/web/frontend/js/features/manage-template-modal/components/settings-template-category.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useMemo, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import getMeta from '../../../utils/meta' -import SettingsMenuSelect from '@/features/editor-left-menu/components/settings/settings-menu-select' -import type { Option } from '@/features/editor-left-menu/components/settings/settings-menu-select' - -interface ManageTemplateCategoryProps { - category: string | null - setCategory: (value: string) => void -} - -export default function SettingsTemplateCategory({ - category, - setCategory -}: ManageTemplateCategoryProps) { - const { t } = useTranslation() - - const { templateLinks } = useMemo( - () => getMeta('ol-ExposedSettings') || [], - [] - ) - - if (templateLinks.length === 0) { - return null - } - - const options: Option[] = useMemo( - () => - templateLinks.map(({ name, url }) => ({ - value: url, - label: name, - })), - [templateLinks] - ) - - useEffect(() => { - if (!category && options.length > 0) { - setCategory(options[0].value) - } - }, [options, category, setCategory]) - - return ( - - ) -} diff --git a/services/web/frontend/js/features/template-gallery/components/gallery-search-sort-header.tsx b/services/web/frontend/js/features/template-gallery/components/gallery-search-sort-header.tsx index 8fe9438a82..128d379684 100644 --- a/services/web/frontend/js/features/template-gallery/components/gallery-search-sort-header.tsx +++ b/services/web/frontend/js/features/template-gallery/components/gallery-search-sort-header.tsx @@ -52,7 +52,7 @@ export default function GallerySearchSortHeader( { gotoAllLink }: { boolean } ) )} - + e.preventDefault()} > void - actionHandler: (editedTemplate: Template) => void | Promise -} - -function EditTemplateModal({ - showModal, - handleCloseModal, - actionHandler, -}: EditTemplateModalProps) { - const { t } = useTranslation() - const { template } = useTemplateContext() - const [editedTemplate, setEditedTemplate] = useState