From aec758b79f85364dbe135bf1e98596fbf53cc4b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:09:26 +0000 Subject: [PATCH 001/411] Initial plan From 7012d268abc0795e69fd2c2ec73fc2e32bce4551 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:15:53 +0000 Subject: [PATCH 002/411] Initial plan From 786eb1b065ac85e99a7abc3d043c77ff81e538fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:28:49 +0000 Subject: [PATCH 003/411] feat: Extract type guards to shared utilities and simplify clipboard logic Co-authored-by: marcuscastelo <27441558+marcuscastelo@users.noreply.github.com> --- src/sections/meal/components/MealEditView.tsx | 35 +++---------------- .../recipe/components/RecipeEditView.tsx | 19 ++-------- .../components/UnifiedRecipeEditView.tsx | 16 ++------- src/shared/utils/typeUtils.ts | 31 ++++++++++++++++ 4 files changed, 41 insertions(+), 60 deletions(-) diff --git a/src/sections/meal/components/MealEditView.tsx b/src/sections/meal/components/MealEditView.tsx index 29627c7a9..0760aa459 100644 --- a/src/sections/meal/components/MealEditView.tsx +++ b/src/sections/meal/components/MealEditView.tsx @@ -28,6 +28,7 @@ import { import { regenerateId } from '~/shared/utils/idUtils' import { logging } from '~/shared/utils/logging' import { calcMealCalories } from '~/shared/utils/macroMath' +import { isMeal, isUnifiedItem } from '~/shared/utils/typeUtils' // TODO: Remove deprecated props and their usages export type MealEditViewProps = { @@ -95,65 +96,39 @@ export function MealEditViewHeader(props: { acceptedClipboardSchema, getDataToCopy: () => meal(), onPaste: (data) => { - // Check if data is already UnifiedItem(s) and handle directly if (Array.isArray(data)) { const firstItem = data[0] - if (firstItem && '__type' in firstItem) { - // Handle array of UnifiedItems - type is already validated by schema + if (firstItem && isUnifiedItem(firstItem)) { const unifiedItemsToAdd = data.map((item) => ({ ...item, id: regenerateId(item).id, })) - - // Update the meal with all items at once const updatedMeal = addItemsToMeal(meal(), unifiedItemsToAdd) props.onUpdateMeal(updatedMeal) return } } - if ( - typeof data === 'object' && - '__type' in data && - data.__type === 'Meal' - ) { - // Handle pasted Meal - extract its items and add them to current meal - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const mealData = data as Meal - logging.debug('Pasting meal with items:', { mealData }) - const unifiedItemsToAdd = mealData.items.map((item) => ({ + if (isMeal(data)) { + const unifiedItemsToAdd = data.items.map((item) => ({ ...item, id: regenerateId(item).id, })) - logging.debug('Items to add:', { unifiedItemsToAdd }) - - // Update the meal with all items at once const updatedMeal = addItemsToMeal(meal(), unifiedItemsToAdd) props.onUpdateMeal(updatedMeal) return } - if ( - typeof data === 'object' && - '__type' in data && - data.__type === 'UnifiedItem' - ) { - // Handle single UnifiedItem - type is already validated by schema + if (isUnifiedItem(data)) { const regeneratedItem = { ...data, id: regenerateId(data).id, } - - // Update the meal with the single item const updatedMeal = addItemsToMeal(meal(), [regeneratedItem]) props.onUpdateMeal(updatedMeal) return } - // Handle other types supported by schema (recipes, etc.) - // Since schema validation passed, this should be a recipe - // For now, we'll skip unsupported formats in paste - // TODO: Add proper recipe-to-items conversion if needed logging.warn('Unsupported paste format:', { data }) }, }) diff --git a/src/sections/recipe/components/RecipeEditView.tsx b/src/sections/recipe/components/RecipeEditView.tsx index ca4aa69dd..19f260a58 100644 --- a/src/sections/recipe/components/RecipeEditView.tsx +++ b/src/sections/recipe/components/RecipeEditView.tsx @@ -29,6 +29,7 @@ import { openClearItemsConfirmModal } from '~/shared/modal/helpers/specializedMo import { regenerateId } from '~/shared/utils/idUtils' import { logging } from '~/shared/utils/logging' import { calcRecipeCalories } from '~/shared/utils/macroMath' +import { isUnifiedItem } from '~/shared/utils/typeUtils' export type RecipeEditViewProps = { recipe: Accessor @@ -65,38 +66,24 @@ export function RecipeEditHeader(props: { acceptedClipboardSchema, getDataToCopy: () => recipe(), onPaste: (data) => { - // Helper function to check if an object is a UnifiedItem - const isUnifiedItem = (obj: unknown): obj is UnifiedItem => { - return ( - typeof obj === 'object' && - obj !== null && - '__type' in obj && - obj.__type === 'UnifiedItem' - ) - } - - // Check if data is array of UnifiedItems if (Array.isArray(data) && data.every(isUnifiedItem)) { const itemsToAdd = data - .filter((item) => item.reference.type === 'food') // Only food items in recipes + .filter((item) => item.reference.type === 'food') .map((item) => regenerateId(item)) const newRecipe = addItemsToRecipe(recipe(), itemsToAdd) props.onUpdateRecipe(newRecipe) return } - // Check if data is single UnifiedItem if (isUnifiedItem(data)) { if (data.reference.type === 'food') { - const item = data - const regeneratedItem = regenerateId(item) + const regeneratedItem = regenerateId(data) const newRecipe = addItemsToRecipe(recipe(), [regeneratedItem]) props.onUpdateRecipe(newRecipe) } return } - // Handle other supported clipboard formats logging.warn('Unsupported paste format:', data) }, }) diff --git a/src/sections/recipe/components/UnifiedRecipeEditView.tsx b/src/sections/recipe/components/UnifiedRecipeEditView.tsx index 2a482f968..a057aab6e 100644 --- a/src/sections/recipe/components/UnifiedRecipeEditView.tsx +++ b/src/sections/recipe/components/UnifiedRecipeEditView.tsx @@ -27,6 +27,7 @@ import { openClearItemsConfirmModal } from '~/shared/modal/helpers/specializedMo import { regenerateId } from '~/shared/utils/idUtils' import { logging } from '~/shared/utils/logging' import { calcRecipeCalories } from '~/shared/utils/macroMath' +import { isUnifiedItem } from '~/shared/utils/typeUtils' export type RecipeEditViewProps = { recipe: Accessor @@ -56,27 +57,15 @@ export function RecipeEditView(props: RecipeEditViewProps) { acceptedClipboardSchema, getDataToCopy: () => [...recipe().items], onPaste: (data) => { - // Helper function to check if an object is a UnifiedItem - const isUnifiedItem = (obj: unknown): obj is UnifiedItem => { - return ( - typeof obj === 'object' && - obj !== null && - '__type' in obj && - obj.__type === 'UnifiedItem' - ) - } - - // Check if data is array of UnifiedItems if (Array.isArray(data) && data.every(isUnifiedItem)) { const itemsToAdd = data - .filter((item) => item.reference.type === 'food') // Only food items in recipes + .filter((item) => item.reference.type === 'food') .map((item) => regenerateId(item)) const newRecipe = addItemsToRecipe(recipe(), itemsToAdd) setRecipe(newRecipe) return } - // Check if data is single UnifiedItem if (isUnifiedItem(data)) { if (data.reference.type === 'food') { const regeneratedItem = regenerateId(data) @@ -86,7 +75,6 @@ export function RecipeEditView(props: RecipeEditViewProps) { return } - // Handle other supported clipboard formats logging.warn('Unsupported paste format:', data) }, }) diff --git a/src/shared/utils/typeUtils.ts b/src/shared/utils/typeUtils.ts index c29593a2b..e826827e9 100644 --- a/src/shared/utils/typeUtils.ts +++ b/src/shared/utils/typeUtils.ts @@ -1,5 +1,36 @@ +import { type Meal } from '~/modules/diet/meal/domain/meal' +import { type Recipe } from '~/modules/diet/recipe/domain/recipe' +import { type UnifiedItem } from '~/modules/diet/unified-item/schema/unifiedItemSchema' + export type Mutable = { -readonly [P in keyof T]: Mutable } export type ObjectValues = T[keyof T] + +export function isUnifiedItem(obj: unknown): obj is UnifiedItem { + return ( + typeof obj === 'object' && + obj !== null && + '__type' in obj && + obj.__type === 'UnifiedItem' + ) +} + +export function isMeal(obj: unknown): obj is Meal { + return ( + typeof obj === 'object' && + obj !== null && + '__type' in obj && + obj.__type === 'Meal' + ) +} + +export function isRecipe(obj: unknown): obj is Recipe { + return ( + typeof obj === 'object' && + obj !== null && + '__type' in obj && + obj.__type === 'Recipe' + ) +} From daaa889cb48aff78a28e9100673bccf0b96d2e8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:30:47 +0000 Subject: [PATCH 004/411] Simplify modal types using Accessor for reactive values Co-authored-by: marcuscastelo <27441558+marcuscastelo@users.noreply.github.com> --- .../components/UnifiedModalContainer.tsx | 27 +++++++--- src/shared/modal/helpers/modalHelpers.ts | 53 ++++++++++++------- src/shared/modal/tests/unifiedModal.test.ts | 26 +++++++++ src/shared/modal/types/modalTypes.ts | 14 ++--- 4 files changed, 88 insertions(+), 32 deletions(-) diff --git a/src/shared/modal/components/UnifiedModalContainer.tsx b/src/shared/modal/components/UnifiedModalContainer.tsx index f12b841ea..40d3c8866 100644 --- a/src/shared/modal/components/UnifiedModalContainer.tsx +++ b/src/shared/modal/components/UnifiedModalContainer.tsx @@ -3,7 +3,7 @@ * Renders all active modals using the existing Modal component. */ -import { For, Show } from 'solid-js' +import { type Accessor, For, Show } from 'solid-js' import { Modal } from '~/sections/common/components/Modal' import { ModalErrorBoundary } from '~/shared/modal/components/ModalErrorBoundary' @@ -11,18 +11,31 @@ import { modals } from '~/shared/modal/core/modalManager' import { closeModal } from '~/shared/modal/helpers/modalHelpers' import type { ModalState } from '~/shared/modal/types/modalTypes' +/** + * Resolves a value that could be static or an Accessor. + * For JSXElement types, this should NOT be used as JSXElement can be a function. + */ +function resolveStringValue( + value: T | Accessor | undefined, +): T | undefined { + if (value === undefined) return undefined + return typeof value === 'function' ? (value as Accessor)() : value +} + /** * Renders individual modal content based on modal type. */ function ModalRenderer(props: ModalState) { + const title = () => resolveStringValue(props.title) + return ( - {props.title} + {title()}
-
{props.title}
+
{title()}
@@ -73,7 +86,9 @@ function ModalRenderer(props: ModalState) {

- {props.type === 'confirmation' ? props.message : ''} + {props.type === 'confirmation' + ? resolveStringValue(props.message) + : ''}

@@ -103,7 +118,7 @@ function ModalRenderer(props: ModalState) { }} > {props.type === 'confirmation' - ? (props.cancelText ?? 'Cancel') + ? resolveStringValue(props.cancelText) ?? 'Cancel' : 'Cancel'} diff --git a/src/shared/modal/helpers/modalHelpers.ts b/src/shared/modal/helpers/modalHelpers.ts index 0cd016939..2f1f8f8cc 100644 --- a/src/shared/modal/helpers/modalHelpers.ts +++ b/src/shared/modal/helpers/modalHelpers.ts @@ -3,26 +3,31 @@ * These provide convenient APIs for frequently used modal operations. */ -import type { JSXElement } from 'solid-js' +import type { Accessor, JSXElement } from 'solid-js' import { modalManager } from '~/shared/modal/core/modalManager' -import type { ModalId, ModalPriority } from '~/shared/modal/types/modalTypes' +import type { + ModalBody, + ModalId, + ModalPriority, + ModalTitle, +} from '~/shared/modal/types/modalTypes' import { logging } from '~/shared/utils/logging' /** * Opens a confirmation modal with standardized styling and behavior. * - * @param message The confirmation message to display + * @param message The confirmation message to display (can be static string or Accessor) * @param options Configuration for the confirmation modal * @returns The modal ID for tracking */ export function openConfirmModal( - message: string, + message: string | Accessor, options: { - title?: string - confirmText?: string - cancelText?: string + title?: ModalTitle | Accessor + confirmText?: string | Accessor + cancelText?: string | Accessor onConfirm: () => void | Promise onCancel?: () => void priority?: ModalPriority @@ -38,7 +43,7 @@ export function openConfirmModal( onConfirm: options.onConfirm, onCancel: options.onCancel, priority: options.priority ?? 'normal', - closeOnOutsideClick: false, // Prevent accidental confirmation + closeOnOutsideClick: false, closeOnEscape: true, showCloseButton: true, }) @@ -56,14 +61,14 @@ export function openConfirmModal( * @returns The modal ID for tracking */ export function openContentModal( - content: JSXElement | ((modalId: ModalId) => JSXElement), + content: ModalBody | ((modalId: ModalId) => ModalBody), options: { - title?: string + title?: ModalTitle | Accessor priority?: ModalPriority closeOnOutsideClick?: boolean closeOnEscape?: boolean showCloseButton?: boolean - footer?: JSXElement | (() => JSXElement) + footer?: ModalBody | Accessor onClose?: () => void } = {}, ): ModalId { @@ -93,28 +98,36 @@ export function openContentModal( * @returns The modal ID for tracking */ export function openEditModal( - content: JSXElement | ((modalId: ModalId) => JSXElement), + content: ModalBody | ((modalId: ModalId) => ModalBody), options: { - title: string - targetName?: string // For nested editing contexts like "Day Diet > Breakfast" + title: ModalTitle | Accessor + targetName?: string onClose?: () => void onSave?: () => void onCancel?: () => void }, ): ModalId { try { - const fullTitle = - options.targetName !== undefined && options.targetName.length > 0 - ? `${options.title} - ${options.targetName}` - : options.title + let fullTitle: ModalTitle | Accessor + + if (options.targetName !== undefined && options.targetName.length > 0) { + if (typeof options.title === 'function') { + fullTitle = () => + `${(options.title as Accessor)()} - ${options.targetName}` + } else { + fullTitle = `${options.title} - ${options.targetName}` + } + } else { + fullTitle = options.title + } return modalManager.openModal({ type: 'content', title: fullTitle, content, priority: 'normal', - closeOnOutsideClick: false, // Prevent accidental loss of edits - closeOnEscape: false, // Require explicit save/cancel + closeOnOutsideClick: false, + closeOnEscape: false, showCloseButton: true, onClose: options.onClose, }) diff --git a/src/shared/modal/tests/unifiedModal.test.ts b/src/shared/modal/tests/unifiedModal.test.ts index cb3466639..13982e827 100644 --- a/src/shared/modal/tests/unifiedModal.test.ts +++ b/src/shared/modal/tests/unifiedModal.test.ts @@ -3,6 +3,7 @@ * Validates that the core functionality works correctly. */ +import { createSignal } from 'solid-js' import { beforeEach, describe, expect, it } from 'vitest' import { @@ -80,6 +81,31 @@ describe('Unified Modal System', () => { void modalManager.closeModal(modalId) }) + it('should support Accessor types for reactive values', () => { + const [title, setTitle] = createSignal('Initial Title') + const [message, setMessage] = createSignal('Initial Message') + + const modalId = modalManager.openModal({ + type: 'confirmation', + title, + message, + onConfirm: () => {}, + }) + + const modalList = modals() + const modal = modalList.find((m) => m.id === modalId) + expect(modal).toBeDefined() + expect(typeof modal?.title).toBe('function') + expect(typeof modal?.message).toBe('function') + + // Verify the modal stores the accessor + if (modal?.type === 'confirmation') { + expect(typeof modal.message).toBe('function') + } + + void modalManager.closeModal(modalId) + }) + it('should track multiple modals in creation order', async () => { const modal1 = modalManager.openModal({ type: 'content', diff --git a/src/shared/modal/types/modalTypes.ts b/src/shared/modal/types/modalTypes.ts index 9d7e441f5..1718cbe6a 100644 --- a/src/shared/modal/types/modalTypes.ts +++ b/src/shared/modal/types/modalTypes.ts @@ -4,10 +4,12 @@ import type { ToastError } from '~/modules/toast/domain/toastTypes' export type ModalId = string export type ModalPriority = 'low' | 'normal' | 'high' | 'critical' +export type ModalTitle = string +export type ModalBody = JSXElement export type BaseModalConfig = { id?: ModalId - title?: string + title?: ModalTitle | Accessor priority?: ModalPriority closeOnOutsideClick?: boolean closeOnEscape?: boolean @@ -24,15 +26,15 @@ export type ErrorModalConfig = BaseModalConfig & { export type ContentModalConfig = BaseModalConfig & { type: 'content' - content: JSXElement | ((modalId: ModalId) => JSXElement) - footer?: JSXElement | (() => JSXElement) + content: ModalBody | ((modalId: ModalId) => ModalBody) + footer?: ModalBody | Accessor } export type ConfirmationModalConfig = BaseModalConfig & { type: 'confirmation' - message: string - confirmText?: string - cancelText?: string + message: string | Accessor + confirmText?: string | Accessor + cancelText?: string | Accessor onConfirm?: () => void | Promise onCancel?: () => void } From 3c889a53b31f30bb51307bb63fc3c749d67f38c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:32:08 +0000 Subject: [PATCH 005/411] Fix TypeScript error in Accessor test Co-authored-by: marcuscastelo <27441558+marcuscastelo@users.noreply.github.com> --- src/shared/modal/tests/unifiedModal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/modal/tests/unifiedModal.test.ts b/src/shared/modal/tests/unifiedModal.test.ts index 13982e827..13da4826a 100644 --- a/src/shared/modal/tests/unifiedModal.test.ts +++ b/src/shared/modal/tests/unifiedModal.test.ts @@ -95,8 +95,8 @@ describe('Unified Modal System', () => { const modalList = modals() const modal = modalList.find((m) => m.id === modalId) expect(modal).toBeDefined() + expect(modal?.type).toBe('confirmation') expect(typeof modal?.title).toBe('function') - expect(typeof modal?.message).toBe('function') // Verify the modal stores the accessor if (modal?.type === 'confirmation') { From 6c43fdf94712e7e0aabc4b6d40062b06906c99f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:24:22 +0000 Subject: [PATCH 006/411] Initial plan From a0141e0847cefba56e488f6ec594efbed0d8f2a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:30:07 +0000 Subject: [PATCH 007/411] Replace property assertion typeguards with __type discriminator checks Co-authored-by: marcuscastelo <27441558+marcuscastelo@users.noreply.github.com> --- src/modules/diet/template/domain/template.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/modules/diet/template/domain/template.ts b/src/modules/diet/template/domain/template.ts index 6b8a6462e..7cc9f0bb8 100644 --- a/src/modules/diet/template/domain/template.ts +++ b/src/modules/diet/template/domain/template.ts @@ -9,9 +9,7 @@ export type Template = Food | Recipe * @returns True if Template is food */ export function isTemplateFood(t: Template): t is Food { - // TODO: Replace property assertion as typeguard with a more reliable alternetive - // Issue URL: https://github.com/marcuscastelo/macroflows/issues/1062 - return 'ean' in t && 'macros' in t && !('user_id' in t) + return t.__type === 'Food' } /** @@ -20,7 +18,5 @@ export function isTemplateFood(t: Template): t is Food { * @returns True if Template is recipe */ export function isTemplateRecipe(t: Template): t is Recipe { - // TODO: Replace property assertion as typeguard with a more reliable alternetive - // Issue URL: https://github.com/marcuscastelo/macroflows/issues/1061 - return 'user_id' in t && 'items' in t && 'prepared_multiplier' in t + return t.__type === 'Recipe' } From 599528b8bafd37ed7c3669b45feab67fa155f4d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:31:37 +0000 Subject: [PATCH 008/411] Replace property assertion typeguards with __type discriminator checks Co-authored-by: marcuscastelo <27441558+marcuscastelo@users.noreply.github.com> --- src/shared/modal/components/UnifiedModalContainer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/modal/components/UnifiedModalContainer.tsx b/src/shared/modal/components/UnifiedModalContainer.tsx index 40d3c8866..bf6ee401d 100644 --- a/src/shared/modal/components/UnifiedModalContainer.tsx +++ b/src/shared/modal/components/UnifiedModalContainer.tsx @@ -19,7 +19,7 @@ function resolveStringValue( value: T | Accessor | undefined, ): T | undefined { if (value === undefined) return undefined - return typeof value === 'function' ? (value as Accessor)() : value + return typeof value === 'function' ? value() : value } /** @@ -118,7 +118,7 @@ function ModalRenderer(props: ModalState) { }} > {props.type === 'confirmation' - ? resolveStringValue(props.cancelText) ?? 'Cancel' + ? (resolveStringValue(props.cancelText) ?? 'Cancel') : 'Cancel'} From ffbc16b473088dd38402a36d89f2f53c76e84b56 Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Tue, 4 Nov 2025 17:55:35 -0300 Subject: [PATCH 009/411] refactor(clipboard): remove internal state and periodic read from clipboard hooks --- src/sections/common/hooks/useClipboard.tsx | 41 ++---- .../common/hooks/useCopyPasteActions.ts | 17 +-- src/sections/meal/components/MealEditView.tsx | 117 +++++++++--------- .../recipe/components/RecipeEditView.tsx | 75 ++++++----- .../components/UnifiedRecipeEditView.tsx | 71 ++++++----- .../components/GroupChildrenEditor.tsx | 99 ++++++++------- .../components/UnifiedItemEditBody.tsx | 1 - .../components/UnifiedItemEditModal.tsx | 16 ++- 8 files changed, 204 insertions(+), 233 deletions(-) diff --git a/src/sections/common/hooks/useClipboard.tsx b/src/sections/common/hooks/useClipboard.tsx index 11bf55512..ade1ee720 100644 --- a/src/sections/common/hooks/useClipboard.tsx +++ b/src/sections/common/hooks/useClipboard.tsx @@ -1,4 +1,3 @@ -import { createEffect, createSignal } from 'solid-js' import { type z } from 'zod/v4' import { @@ -19,20 +18,16 @@ export function useClipboard(props?: { periodicRead?: boolean }) { const filter = () => props?.filter - const periodicRead = () => props?.periodicRead ?? true - const [clipboard, setClipboard] = createSignal('') const handleWrite = (text: string, onError?: (error: unknown) => void) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (window.navigator.clipboard === undefined) { showError(`Clipboard API not supported`) - setClipboard('') return } window.navigator.clipboard .writeText(text) .then(() => { - setClipboard(text) if (text.length > 0) { showSuccess(`Copiado com sucesso`) } @@ -46,41 +41,23 @@ export function useClipboard(props?: { }) } - const handleRead = () => { - const afterRead = (newClipboard: string) => { - const filter_ = filter() - if (filter_ !== undefined && !filter_(newClipboard)) { - setClipboard('') - return - } - - setClipboard(newClipboard) - } + const handleRead = async () => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (window.navigator.clipboard === undefined) { - // Clipboard API not supported, set empty clipboard - setClipboard('') - return + return '' } - window.navigator.clipboard + const clipboardText = await window.navigator.clipboard .readText() - .then(afterRead) - .catch(() => { - // Do nothing. This is expected when the DOM is not focused - }) - } - // Update clipboard periodically - createEffect(() => { - if (!periodicRead()) return + .catch(() => '') - const interval = setInterval(handleRead, 1000) - return () => { - clearInterval(interval) + if (filter()?.(clipboardText) ?? true) { + return clipboardText } - }) + + return '' + } return { - clipboard, write: handleWrite, read: handleRead, clear: () => { diff --git a/src/sections/common/hooks/useCopyPasteActions.ts b/src/sections/common/hooks/useCopyPasteActions.ts index 6af8955a3..80e13fd9c 100644 --- a/src/sections/common/hooks/useCopyPasteActions.ts +++ b/src/sections/common/hooks/useCopyPasteActions.ts @@ -25,19 +25,22 @@ export function useCopyPasteActions({ }) { const isClipboardValid = createClipboardSchemaFilter(acceptedClipboardSchema) const { - clipboard: clipboardText, + read: readFromClipboard, write: writeToClipboard, clear: clearClipboard, - } = useClipboard({ filter: isClipboardValid }) + } = useClipboard({ + filter: isClipboardValid, + }) const handleCopy = () => { writeToClipboard(JSON.stringify(getDataToCopy())) } - const handlePasteAfterConfirm = () => { - const data = deserializeClipboard(clipboardText(), acceptedClipboardSchema) + const handlePasteAfterConfirm = async () => { + const clipboardText = await readFromClipboard() + const data = deserializeClipboard(clipboardText, acceptedClipboardSchema) if (data === null) { - throw new Error('Invalid clipboard data: ' + clipboardText()) + throw new Error('Invalid clipboard data: ' + clipboardText) } onPaste(data) clearClipboard() @@ -52,10 +55,10 @@ export function useCopyPasteActions({ }) } - const hasValidPastableOnClipboard = () => isClipboardValid(clipboardText()) + const hasValidPastableOnClipboard = async () => + isClipboardValid(await readFromClipboard()) return { - clipboardText, writeToClipboard, clearClipboard, handleCopy, diff --git a/src/sections/meal/components/MealEditView.tsx b/src/sections/meal/components/MealEditView.tsx index 29627c7a9..d9c81c625 100644 --- a/src/sections/meal/components/MealEditView.tsx +++ b/src/sections/meal/components/MealEditView.tsx @@ -90,73 +90,72 @@ export function MealEditViewHeader(props: { .or(unifiedItemSchema) .or(z.array(unifiedItemSchema)) - const { handleCopy, handlePaste, hasValidPastableOnClipboard } = - useCopyPasteActions({ - acceptedClipboardSchema, - getDataToCopy: () => meal(), - onPaste: (data) => { - // Check if data is already UnifiedItem(s) and handle directly - if (Array.isArray(data)) { - const firstItem = data[0] - if (firstItem && '__type' in firstItem) { - // Handle array of UnifiedItems - type is already validated by schema - const unifiedItemsToAdd = data.map((item) => ({ - ...item, - id: regenerateId(item).id, - })) - - // Update the meal with all items at once - const updatedMeal = addItemsToMeal(meal(), unifiedItemsToAdd) - props.onUpdateMeal(updatedMeal) - return - } - } - - if ( - typeof data === 'object' && - '__type' in data && - data.__type === 'Meal' - ) { - // Handle pasted Meal - extract its items and add them to current meal - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const mealData = data as Meal - logging.debug('Pasting meal with items:', { mealData }) - const unifiedItemsToAdd = mealData.items.map((item) => ({ + const { handleCopy, handlePaste } = useCopyPasteActions({ + acceptedClipboardSchema, + getDataToCopy: () => meal(), + onPaste: (data) => { + // Check if data is already UnifiedItem(s) and handle directly + if (Array.isArray(data)) { + const firstItem = data[0] + if (firstItem && '__type' in firstItem) { + // Handle array of UnifiedItems - type is already validated by schema + const unifiedItemsToAdd = data.map((item) => ({ ...item, id: regenerateId(item).id, })) - logging.debug('Items to add:', { unifiedItemsToAdd }) // Update the meal with all items at once const updatedMeal = addItemsToMeal(meal(), unifiedItemsToAdd) props.onUpdateMeal(updatedMeal) return } - - if ( - typeof data === 'object' && - '__type' in data && - data.__type === 'UnifiedItem' - ) { - // Handle single UnifiedItem - type is already validated by schema - const regeneratedItem = { - ...data, - id: regenerateId(data).id, - } - - // Update the meal with the single item - const updatedMeal = addItemsToMeal(meal(), [regeneratedItem]) - props.onUpdateMeal(updatedMeal) - return + } + + if ( + typeof data === 'object' && + '__type' in data && + data.__type === 'Meal' + ) { + // Handle pasted Meal - extract its items and add them to current meal + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const mealData = data as Meal + logging.debug('Pasting meal with items:', { mealData }) + const unifiedItemsToAdd = mealData.items.map((item) => ({ + ...item, + id: regenerateId(item).id, + })) + logging.debug('Items to add:', { unifiedItemsToAdd }) + + // Update the meal with all items at once + const updatedMeal = addItemsToMeal(meal(), unifiedItemsToAdd) + props.onUpdateMeal(updatedMeal) + return + } + + if ( + typeof data === 'object' && + '__type' in data && + data.__type === 'UnifiedItem' + ) { + // Handle single UnifiedItem - type is already validated by schema + const regeneratedItem = { + ...data, + id: regenerateId(data).id, } - // Handle other types supported by schema (recipes, etc.) - // Since schema validation passed, this should be a recipe - // For now, we'll skip unsupported formats in paste - // TODO: Add proper recipe-to-items conversion if needed - logging.warn('Unsupported paste format:', { data }) - }, - }) + // Update the meal with the single item + const updatedMeal = addItemsToMeal(meal(), [regeneratedItem]) + props.onUpdateMeal(updatedMeal) + return + } + + // Handle other types supported by schema (recipes, etc.) + // Since schema validation passed, this should be a recipe + // For now, we'll skip unsupported formats in paste + // TODO: Add proper recipe-to-items conversion if needed + logging.warn('Unsupported paste format:', { data }) + }, + }) const mealCalories = () => calcMealCalories(meal()) @@ -181,10 +180,8 @@ export function MealEditViewHeader(props: { {props.mode !== 'summary' && ( 0 - } - canPaste={hasValidPastableOnClipboard()} + canCopy={mealSignal().items.length > 0} + canPaste={true} canClear={mealSignal().items.length > 0} onCopy={handleCopy} onPaste={handlePaste} diff --git a/src/sections/recipe/components/RecipeEditView.tsx b/src/sections/recipe/components/RecipeEditView.tsx index ca4aa69dd..3becb67d2 100644 --- a/src/sections/recipe/components/RecipeEditView.tsx +++ b/src/sections/recipe/components/RecipeEditView.tsx @@ -60,46 +60,45 @@ export function RecipeEditHeader(props: { const { recipe } = useRecipeEditContext() - const { handleCopy, handlePaste, hasValidPastableOnClipboard } = - useCopyPasteActions({ - acceptedClipboardSchema, - getDataToCopy: () => recipe(), - onPaste: (data) => { - // Helper function to check if an object is a UnifiedItem - const isUnifiedItem = (obj: unknown): obj is UnifiedItem => { - return ( - typeof obj === 'object' && - obj !== null && - '__type' in obj && - obj.__type === 'UnifiedItem' - ) - } + const { handleCopy, handlePaste } = useCopyPasteActions({ + acceptedClipboardSchema, + getDataToCopy: () => recipe(), + onPaste: (data) => { + // Helper function to check if an object is a UnifiedItem + const isUnifiedItem = (obj: unknown): obj is UnifiedItem => { + return ( + typeof obj === 'object' && + obj !== null && + '__type' in obj && + obj.__type === 'UnifiedItem' + ) + } - // Check if data is array of UnifiedItems - if (Array.isArray(data) && data.every(isUnifiedItem)) { - const itemsToAdd = data - .filter((item) => item.reference.type === 'food') // Only food items in recipes - .map((item) => regenerateId(item)) - const newRecipe = addItemsToRecipe(recipe(), itemsToAdd) - props.onUpdateRecipe(newRecipe) - return - } + // Check if data is array of UnifiedItems + if (Array.isArray(data) && data.every(isUnifiedItem)) { + const itemsToAdd = data + .filter((item) => item.reference.type === 'food') // Only food items in recipes + .map((item) => regenerateId(item)) + const newRecipe = addItemsToRecipe(recipe(), itemsToAdd) + props.onUpdateRecipe(newRecipe) + return + } - // Check if data is single UnifiedItem - if (isUnifiedItem(data)) { - if (data.reference.type === 'food') { - const item = data - const regeneratedItem = regenerateId(item) - const newRecipe = addItemsToRecipe(recipe(), [regeneratedItem]) - props.onUpdateRecipe(newRecipe) - } - return + // Check if data is single UnifiedItem + if (isUnifiedItem(data)) { + if (data.reference.type === 'food') { + const item = data + const regeneratedItem = regenerateId(item) + const newRecipe = addItemsToRecipe(recipe(), [regeneratedItem]) + props.onUpdateRecipe(newRecipe) } + return + } - // Handle other supported clipboard formats - logging.warn('Unsupported paste format:', data) - }, - }) + // Handle other supported clipboard formats + logging.warn('Unsupported paste format:', data) + }, + }) const recipeCalories = calcRecipeCalories(recipe()) @@ -121,8 +120,8 @@ export function RecipeEditHeader(props: {

{recipeCalories.toFixed(0)}kcal

0} - canPaste={hasValidPastableOnClipboard()} + canCopy={recipe().items.length > 0} + canPaste={true} canClear={recipe().items.length > 0} onCopy={handleCopy} onPaste={handlePaste} diff --git a/src/sections/recipe/components/UnifiedRecipeEditView.tsx b/src/sections/recipe/components/UnifiedRecipeEditView.tsx index 2a482f968..b62b1755b 100644 --- a/src/sections/recipe/components/UnifiedRecipeEditView.tsx +++ b/src/sections/recipe/components/UnifiedRecipeEditView.tsx @@ -51,45 +51,44 @@ export function RecipeEditView(props: RecipeEditViewProps) { mealSchema, ]) - const { handleCopy, handlePaste, hasValidPastableOnClipboard } = - useCopyPasteActions({ - acceptedClipboardSchema, - getDataToCopy: () => [...recipe().items], - onPaste: (data) => { - // Helper function to check if an object is a UnifiedItem - const isUnifiedItem = (obj: unknown): obj is UnifiedItem => { - return ( - typeof obj === 'object' && - obj !== null && - '__type' in obj && - obj.__type === 'UnifiedItem' - ) - } + const { handleCopy, handlePaste } = useCopyPasteActions({ + acceptedClipboardSchema, + getDataToCopy: () => [...recipe().items], + onPaste: (data) => { + // Helper function to check if an object is a UnifiedItem + const isUnifiedItem = (obj: unknown): obj is UnifiedItem => { + return ( + typeof obj === 'object' && + obj !== null && + '__type' in obj && + obj.__type === 'UnifiedItem' + ) + } - // Check if data is array of UnifiedItems - if (Array.isArray(data) && data.every(isUnifiedItem)) { - const itemsToAdd = data - .filter((item) => item.reference.type === 'food') // Only food items in recipes - .map((item) => regenerateId(item)) - const newRecipe = addItemsToRecipe(recipe(), itemsToAdd) - setRecipe(newRecipe) - return - } + // Check if data is array of UnifiedItems + if (Array.isArray(data) && data.every(isUnifiedItem)) { + const itemsToAdd = data + .filter((item) => item.reference.type === 'food') // Only food items in recipes + .map((item) => regenerateId(item)) + const newRecipe = addItemsToRecipe(recipe(), itemsToAdd) + setRecipe(newRecipe) + return + } - // Check if data is single UnifiedItem - if (isUnifiedItem(data)) { - if (data.reference.type === 'food') { - const regeneratedItem = regenerateId(data) - const newRecipe = addItemsToRecipe(recipe(), [regeneratedItem]) - setRecipe(newRecipe) - } - return + // Check if data is single UnifiedItem + if (isUnifiedItem(data)) { + if (data.reference.type === 'food') { + const regeneratedItem = regenerateId(data) + const newRecipe = addItemsToRecipe(recipe(), [regeneratedItem]) + setRecipe(newRecipe) } + return + } - // Handle other supported clipboard formats - logging.warn('Unsupported paste format:', data) - }, - }) + // Handle other supported clipboard formats + logging.warn('Unsupported paste format:', data) + }, + }) const recipeCalories = calcRecipeCalories(recipe()) @@ -108,7 +107,7 @@ export function RecipeEditView(props: RecipeEditViewProps) { {props.header} 0} - canPaste={hasValidPastableOnClipboard()} + canPaste={true} canClear={recipe().items.length > 0} onCopy={handleCopy} onPaste={handlePaste} diff --git a/src/sections/unified-item/components/GroupChildrenEditor.tsx b/src/sections/unified-item/components/GroupChildrenEditor.tsx index 4c2c63e8f..24fede150 100644 --- a/src/sections/unified-item/components/GroupChildrenEditor.tsx +++ b/src/sections/unified-item/components/GroupChildrenEditor.tsx @@ -51,59 +51,58 @@ export function GroupChildrenEditor(props: GroupChildrenEditorProps) { ) // Clipboard actions for children - const { handleCopy, handlePaste, hasValidPastableOnClipboard } = - useCopyPasteActions({ - acceptedClipboardSchema, - getDataToCopy: () => children(), - onPaste: (data) => { - const itemsToAdd = Array.isArray(data) ? data : [data] - - let updatedItem = props.item() - - // Check if we need to transform a food item into a group - if (isFoodItem(updatedItem) && itemsToAdd.length > 0) { - // Transform the food item into a group with the original food as the first child - const originalAsChild = createUnifiedItem({ - id: generateId(), // New ID for the child - name: updatedItem.name, - quantity: updatedItem.quantity, - reference: updatedItem.reference, // Keep the food reference - }) - - // Create new group with the original food as first child - updatedItem = createUnifiedItem({ - id: updatedItem.id, // Keep the same ID for the parent - name: updatedItem.name, - quantity: updatedItem.quantity, - reference: { - type: 'group', - children: [originalAsChild], - }, - }) + const { handleCopy, handlePaste } = useCopyPasteActions({ + acceptedClipboardSchema, + getDataToCopy: () => children(), + onPaste: (data) => { + const itemsToAdd = Array.isArray(data) ? data : [data] + + let updatedItem = props.item() + + // Check if we need to transform a food item into a group + if (isFoodItem(updatedItem) && itemsToAdd.length > 0) { + // Transform the food item into a group with the original food as the first child + const originalAsChild = createUnifiedItem({ + id: generateId(), // New ID for the child + name: updatedItem.name, + quantity: updatedItem.quantity, + reference: updatedItem.reference, // Keep the food reference + }) + + // Create new group with the original food as first child + updatedItem = createUnifiedItem({ + id: updatedItem.id, // Keep the same ID for the parent + name: updatedItem.name, + quantity: updatedItem.quantity, + reference: { + type: 'group', + children: [originalAsChild], + }, + }) + } + + for (const newChild of itemsToAdd) { + // Regenerate ID to avoid conflicts + const childWithNewId = { + ...newChild, + id: regenerateId(newChild).id, } - for (const newChild of itemsToAdd) { - // Regenerate ID to avoid conflicts - const childWithNewId = { - ...newChild, - id: regenerateId(newChild).id, - } - - // Validate hierarchy to prevent circular references - const tempItem = addChildToItem(updatedItem, childWithNewId) - if (!validateItemHierarchy(tempItem)) { - logging.warn( - `Skipping item ${childWithNewId.name} - would create circular reference`, - ) - continue - } - - updatedItem = tempItem + // Validate hierarchy to prevent circular references + const tempItem = addChildToItem(updatedItem, childWithNewId) + if (!validateItemHierarchy(tempItem)) { + logging.warn( + `Skipping item ${childWithNewId.name} - would create circular reference`, + ) + continue } - props.setItem(updatedItem) - }, - }) + updatedItem = tempItem + } + + props.setItem(updatedItem) + }, + }) const updateChildQuantity = (childId: number, newQuantity: number) => { logging.debug('[GroupChildrenEditor] updateChildQuantity', { @@ -193,7 +192,7 @@ export function GroupChildrenEditor(props: GroupChildrenEditorProps) { {/* Clipboard Actions */} 0} - canPaste={hasValidPastableOnClipboard()} + canPaste={true} canClear={false} // We don't need clear functionality here onCopy={handleCopy} onPaste={handlePaste} diff --git a/src/sections/unified-item/components/UnifiedItemEditBody.tsx b/src/sections/unified-item/components/UnifiedItemEditBody.tsx index e2fd1766e..04cdef757 100644 --- a/src/sections/unified-item/components/UnifiedItemEditBody.tsx +++ b/src/sections/unified-item/components/UnifiedItemEditBody.tsx @@ -31,7 +31,6 @@ export type UnifiedItemEditBodyProps = { clipboardActions?: { onCopy: () => void onPaste: () => void - hasValidPastableOnClipboard: boolean } onAddNewItem?: () => void showAddItemButton?: boolean diff --git a/src/sections/unified-item/components/UnifiedItemEditModal.tsx b/src/sections/unified-item/components/UnifiedItemEditModal.tsx index bbd0beeda..4fda7a820 100644 --- a/src/sections/unified-item/components/UnifiedItemEditModal.tsx +++ b/src/sections/unified-item/components/UnifiedItemEditModal.tsx @@ -229,14 +229,13 @@ export const UnifiedItemEditModal = (_props: UnifiedItemEditModalProps) => { } // Clipboard functionality - const { handleCopy, handlePaste, hasValidPastableOnClipboard } = - useCopyPasteActions({ - acceptedClipboardSchema: unifiedItemSchema, - getDataToCopy: () => item(), - onPaste: (data) => { - setItem(data) - }, - }) + const { handleCopy, handlePaste } = useCopyPasteActions({ + acceptedClipboardSchema: unifiedItemSchema, + getDataToCopy: () => item(), + onPaste: (data) => { + setItem(data) + }, + }) return (
@@ -327,7 +326,6 @@ export const UnifiedItemEditModal = (_props: UnifiedItemEditModalProps) => { clipboardActions={{ onCopy: handleCopy, onPaste: handlePaste, - hasValidPastableOnClipboard: hasValidPastableOnClipboard(), }} onAddNewItem={() => { openTemplateSearchModal({ From 1b6911b1afc6a6b74672de184e716858befbe60c Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Tue, 4 Nov 2025 17:55:50 -0300 Subject: [PATCH 010/411] refactor(modal): simplify type handling and remove unused code --- src/shared/modal/components/UnifiedModalContainer.tsx | 6 +++--- src/shared/modal/helpers/modalHelpers.ts | 3 ++- src/shared/modal/tests/unifiedModal.test.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/shared/modal/components/UnifiedModalContainer.tsx b/src/shared/modal/components/UnifiedModalContainer.tsx index 40d3c8866..bf6ee401d 100644 --- a/src/shared/modal/components/UnifiedModalContainer.tsx +++ b/src/shared/modal/components/UnifiedModalContainer.tsx @@ -19,7 +19,7 @@ function resolveStringValue( value: T | Accessor | undefined, ): T | undefined { if (value === undefined) return undefined - return typeof value === 'function' ? (value as Accessor)() : value + return typeof value === 'function' ? value() : value } /** @@ -118,7 +118,7 @@ function ModalRenderer(props: ModalState) { }} > {props.type === 'confirmation' - ? resolveStringValue(props.cancelText) ?? 'Cancel' + ? (resolveStringValue(props.cancelText) ?? 'Cancel') : 'Cancel'} diff --git a/src/shared/modal/helpers/modalHelpers.ts b/src/shared/modal/helpers/modalHelpers.ts index 2f1f8f8cc..4dcefdbcd 100644 --- a/src/shared/modal/helpers/modalHelpers.ts +++ b/src/shared/modal/helpers/modalHelpers.ts @@ -3,7 +3,7 @@ * These provide convenient APIs for frequently used modal operations. */ -import type { Accessor, JSXElement } from 'solid-js' +import type { Accessor } from 'solid-js' import { modalManager } from '~/shared/modal/core/modalManager' import type { @@ -113,6 +113,7 @@ export function openEditModal( if (options.targetName !== undefined && options.targetName.length > 0) { if (typeof options.title === 'function') { fullTitle = () => + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions `${(options.title as Accessor)()} - ${options.targetName}` } else { fullTitle = `${options.title} - ${options.targetName}` diff --git a/src/shared/modal/tests/unifiedModal.test.ts b/src/shared/modal/tests/unifiedModal.test.ts index 13da4826a..4ce2e3215 100644 --- a/src/shared/modal/tests/unifiedModal.test.ts +++ b/src/shared/modal/tests/unifiedModal.test.ts @@ -82,8 +82,8 @@ describe('Unified Modal System', () => { }) it('should support Accessor types for reactive values', () => { - const [title, setTitle] = createSignal('Initial Title') - const [message, setMessage] = createSignal('Initial Message') + const [title] = createSignal('Initial Title') + const [message] = createSignal('Initial Message') const modalId = modalManager.openModal({ type: 'confirmation', From a0895f68631a1df236258cfa8f1c85169de9688f Mon Sep 17 00:00:00 2001 From: marcuscastelo Date: Tue, 4 Nov 2025 18:08:52 -0300 Subject: [PATCH 011/411] feat(clipboard): handle paste events directly from UI --- .../common/hooks/useCopyPasteActions.ts | 58 ++++++++++++++++--- src/sections/meal/components/MealEditView.tsx | 2 +- .../recipe/components/RecipeEditView.tsx | 2 +- .../components/UnifiedRecipeEditView.tsx | 6 +- .../components/GroupChildrenEditor.tsx | 2 +- .../components/UnifiedItemEditModal.tsx | 2 +- 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/sections/common/hooks/useCopyPasteActions.ts b/src/sections/common/hooks/useCopyPasteActions.ts index 80e13fd9c..edfcf56c7 100644 --- a/src/sections/common/hooks/useCopyPasteActions.ts +++ b/src/sections/common/hooks/useCopyPasteActions.ts @@ -7,13 +7,6 @@ import { import { openConfirmModal } from '~/shared/modal/helpers/modalHelpers' import { deserializeClipboard } from '~/shared/utils/clipboardUtils' -/** - * Shared clipboard copy/paste logic for meal/recipe editors. - * @param options - * - acceptedClipboardSchema: zod schema for validation - * - getDataToCopy: function to get the data to copy - * - onPaste: function to handle parsed clipboard data - */ export function useCopyPasteActions({ acceptedClipboardSchema, getDataToCopy, @@ -46,7 +39,56 @@ export function useCopyPasteActions({ clearClipboard() } - const handlePaste = () => { + type PasteEventLike = + | ClipboardEvent + | { + clipboardData?: DataTransfer | null + preventDefault?: () => void + } + + const handlePaste = (pasteEvent?: PasteEventLike) => { + const processClipboardText = async ( + clipboardText: string, + clearWhenFromApi = true, + ) => { + const data = deserializeClipboard(clipboardText, acceptedClipboardSchema) + if (data === null) { + throw new Error('Invalid clipboard data: ' + clipboardText) + } + onPaste(data) + if (clearWhenFromApi) clearClipboard() + } + + let eventClipboardText: string | null = null + if ( + pasteEvent && + 'clipboardData' in pasteEvent && + pasteEvent.clipboardData + ) { + try { + eventClipboardText = pasteEvent.clipboardData.getData('text') || null + } catch { + eventClipboardText = null + } + } + + if (eventClipboardText !== null) { + try { + pasteEvent?.preventDefault?.() + } catch { + // ignore if cannot prevent + } + + openConfirmModal('Tem certeza que deseja colar os itens?', { + title: 'Colar itens', + confirmText: 'Colar', + cancelText: 'Cancelar', + onConfirm: () => processClipboardText(eventClipboardText, false), + }) + return + } + + // No event-provided clipboard data: use the previous flow (confirmation -> clipboard API) openConfirmModal('Tem certeza que deseja colar os itens?', { title: 'Colar itens', confirmText: 'Colar', diff --git a/src/sections/meal/components/MealEditView.tsx b/src/sections/meal/components/MealEditView.tsx index d9c81c625..da4d19c9d 100644 --- a/src/sections/meal/components/MealEditView.tsx +++ b/src/sections/meal/components/MealEditView.tsx @@ -173,7 +173,7 @@ export function MealEditViewHeader(props: { return ( {(mealSignal) => ( -
+
handlePaste(e)}>
{mealSignal().name}

{mealCalories().toFixed(0)}kcal

diff --git a/src/sections/recipe/components/RecipeEditView.tsx b/src/sections/recipe/components/RecipeEditView.tsx index 3becb67d2..d6fe92170 100644 --- a/src/sections/recipe/components/RecipeEditView.tsx +++ b/src/sections/recipe/components/RecipeEditView.tsx @@ -114,7 +114,7 @@ export function RecipeEditHeader(props: { } return ( -
+
handlePaste(e)}>
{recipe().name}

{recipeCalories.toFixed(0)}kcal

diff --git a/src/sections/recipe/components/UnifiedRecipeEditView.tsx b/src/sections/recipe/components/UnifiedRecipeEditView.tsx index b62b1755b..52e3e786a 100644 --- a/src/sections/recipe/components/UnifiedRecipeEditView.tsx +++ b/src/sections/recipe/components/UnifiedRecipeEditView.tsx @@ -103,7 +103,11 @@ export function RecipeEditView(props: RecipeEditViewProps) { } return ( -
+
handlePaste(e)} + > {props.header} 0} diff --git a/src/sections/unified-item/components/GroupChildrenEditor.tsx b/src/sections/unified-item/components/GroupChildrenEditor.tsx index 24fede150..1906ca490 100644 --- a/src/sections/unified-item/components/GroupChildrenEditor.tsx +++ b/src/sections/unified-item/components/GroupChildrenEditor.tsx @@ -200,7 +200,7 @@ export function GroupChildrenEditor(props: GroupChildrenEditorProps) { />
-
+
handlePaste(e)}> {(child) => ( { return (
-
+
handlePaste(e)}> Date: Tue, 4 Nov 2025 18:57:36 -0300 Subject: [PATCH 012/411] feat(clipboard): implement useCopyPasteActions hook for clipboard management --- ...asteActions.ts => useCopyPasteActions.tsx} | 69 ++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) rename src/sections/common/hooks/{useCopyPasteActions.ts => useCopyPasteActions.tsx} (58%) diff --git a/src/sections/common/hooks/useCopyPasteActions.ts b/src/sections/common/hooks/useCopyPasteActions.tsx similarity index 58% rename from src/sections/common/hooks/useCopyPasteActions.ts rename to src/sections/common/hooks/useCopyPasteActions.tsx index edfcf56c7..fa22f1abb 100644 --- a/src/sections/common/hooks/useCopyPasteActions.ts +++ b/src/sections/common/hooks/useCopyPasteActions.tsx @@ -4,7 +4,11 @@ import { createClipboardSchemaFilter, useClipboard, } from '~/sections/common/hooks/useClipboard' -import { openConfirmModal } from '~/shared/modal/helpers/modalHelpers' +import { + closeModal, + openConfirmModal, + openContentModal, +} from '~/shared/modal/helpers/modalHelpers' import { deserializeClipboard } from '~/shared/utils/clipboardUtils' export function useCopyPasteActions({ @@ -88,13 +92,62 @@ export function useCopyPasteActions({ return } - // No event-provided clipboard data: use the previous flow (confirmation -> clipboard API) - openConfirmModal('Tem certeza que deseja colar os itens?', { - title: 'Colar itens', - confirmText: 'Colar', - cancelText: 'Cancelar', - onConfirm: handlePasteAfterConfirm, - }) + const modalId = openContentModal((mid) => ( +
+

+ Cole os itens agora (pressione Ctrl/Cmd+V) +

+
{ + try { + const text = e.clipboardData?.getData('text') ?? '' + void closeModal(mid) + void processClipboardText(text, false) + } catch { + // ignore and keep modal open + } + }} + > +
+ Foco aqui — pressione Ctrl/Cmd+V +
+
+ +
+ + +
+
+ )) + + // Focus the paste target element so the user can immediately press Ctrl/Cmd+V + setTimeout(() => { + const el = document.getElementById(`paste-target-${modalId}`) + el?.focus() + }, 0) } const hasValidPastableOnClipboard = async () => From 1806bf4bcbd9811fb631e64068452ae99777a78e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:16:36 +0000 Subject: [PATCH 013/411] Initial plan From 313aaf703eef6cd7a8c17e739faa3321ffa10aea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:24:14 +0000 Subject: [PATCH 014/411] Initial exploration and planning for legacy error system cleanup Co-authored-by: marcuscastelo <27441558+marcuscastelo@users.noreply.github.com> --- src/app-version.ts | 6 +----- src/shared/modal/components/UnifiedModalContainer.tsx | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/app-version.ts b/src/app-version.ts index 74c4a2bb7..0e1388059 100644 --- a/src/app-version.ts +++ b/src/app-version.ts @@ -1,5 +1 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import appVersionJson from '~/app-version.json' - -export const APP_VERSION = appVersionJson.version +export const APP_VERSION = '1806bf4' diff --git a/src/shared/modal/components/UnifiedModalContainer.tsx b/src/shared/modal/components/UnifiedModalContainer.tsx index 40d3c8866..bf6ee401d 100644 --- a/src/shared/modal/components/UnifiedModalContainer.tsx +++ b/src/shared/modal/components/UnifiedModalContainer.tsx @@ -19,7 +19,7 @@ function resolveStringValue( value: T | Accessor | undefined, ): T | undefined { if (value === undefined) return undefined - return typeof value === 'function' ? (value as Accessor)() : value + return typeof value === 'function' ? value() : value } /** @@ -118,7 +118,7 @@ function ModalRenderer(props: ModalState) { }} > {props.type === 'confirmation' - ? resolveStringValue(props.cancelText) ?? 'Cancel' + ? (resolveStringValue(props.cancelText) ?? 'Cancel') : 'Cancel'} From 3febd40dd6c0cca1d1a48edf1d55a397b179b7ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:27:20 +0000 Subject: [PATCH 015/411] Initial plan From e0de7a7b59324eeb10a87eb54e51eff62c93cb0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:27:42 +0000 Subject: [PATCH 016/411] Remove legacy error handling components (ErrorDetailModal, clipboard utils) Co-authored-by: marcuscastelo <27441558+marcuscastelo@users.noreply.github.com> --- src/modules/toast/domain/toastMessages.ts | 2 - .../infrastructure/clipboardErrorUtils.ts | 82 ------- .../toast/tests/clipboardErrorUtils.test.ts | 137 ----------- src/modules/toast/ui/ExpandableErrorToast.tsx | 17 -- .../common/components/ErrorDetailModal.tsx | 230 ------------------ 5 files changed, 468 deletions(-) delete mode 100644 src/modules/toast/infrastructure/clipboardErrorUtils.ts delete mode 100644 src/modules/toast/tests/clipboardErrorUtils.test.ts delete mode 100644 src/sections/common/components/ErrorDetailModal.tsx diff --git a/src/modules/toast/domain/toastMessages.ts b/src/modules/toast/domain/toastMessages.ts index 10ff0d625..a105309a4 100644 --- a/src/modules/toast/domain/toastMessages.ts +++ b/src/modules/toast/domain/toastMessages.ts @@ -9,8 +9,6 @@ export const TOAST_MESSAGES = { FALLBACK_ERROR_DETAILS: 'Nenhum detalhe do erro fornecido', SHOW_DETAILS: 'Mostrar detalhes', HIDE_DETAILS: 'Ocultar detalhes', - COPY_ERROR: 'Copiar erro', - COPIED: 'Copiado!', ERROR_TITLE: 'Erro', SUCCESS_TITLE: 'Sucesso', WARNING_TITLE: 'Aviso', diff --git a/src/modules/toast/infrastructure/clipboardErrorUtils.ts b/src/modules/toast/infrastructure/clipboardErrorUtils.ts deleted file mode 100644 index 60bca7b07..000000000 --- a/src/modules/toast/infrastructure/clipboardErrorUtils.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Utilities for formatting and copying error details to clipboard. - * Shared between ExpandableErrorToast and ErrorDetailModal. - */ -import { TOAST_MESSAGES } from '~/modules/toast/domain/toastMessages' -import { type ToastError } from '~/modules/toast/domain/toastTypes' -import { useClipboard } from '~/sections/common/hooks/useClipboard' -import { logging } from '~/shared/utils/logging' - -/** - * Context for clipboard error operations. - * @property component The component name. - * @property operation The operation being performed. - */ -type ClipboardErrorContext = { component: string; operation: string } - -/** - * Formats error details for copying to clipboard. - * @param errorDetails The error details to format. - * @returns A formatted string for clipboard. - */ - -export function formatErrorForClipboard(errorDetails: ToastError): string { - const sections: string[] = [] - if ( - typeof errorDetails.message === 'string' && - errorDetails.message.trim() !== '' - ) { - sections.push(`${TOAST_MESSAGES.ERROR_TITLE}: ${errorDetails.message}`) - } - if ( - typeof errorDetails.fullError === 'string' && - errorDetails.fullError.trim() !== '' && - errorDetails.fullError !== errorDetails.message - ) { - sections.push(`${TOAST_MESSAGES.SHOW_DETAILS}: ${errorDetails.fullError}`) - } - if (errorDetails.context && Object.keys(errorDetails.context).length > 0) { - sections.push( - `${TOAST_MESSAGES.COPY_ERROR}: ${JSON.stringify( - errorDetails.context, - null, - 2, - )}`, - ) - } - if ( - typeof errorDetails.stack === 'string' && - errorDetails.stack.trim() !== '' - ) { - sections.push(`Stack Trace:\n${errorDetails.stack}`) - } - if ( - typeof errorDetails.timestamp === 'number' && - !isNaN(errorDetails.timestamp) && - errorDetails.timestamp > 0 - ) { - sections.push( - `Timestamp: ${new Date(errorDetails.timestamp).toISOString()}`, - ) - } - return `Error Report - ${new Date().toISOString()}\n` + sections.join('\n') -} - -/** - * Handles copying error details to clipboard and error reporting. - * @param errorDetails The error details to copy. - * @param context The context for error handling. - */ -// TODO: use _context -export async function handleCopyErrorToClipboard( - errorDetails: ToastError, - _context: ClipboardErrorContext, -) { - const { write } = useClipboard() - const clipboardContent = formatErrorForClipboard(errorDetails) - write(clipboardContent, (error) => { - if (error !== null) { - logging.error('Toast operation error:', error) - } - }) -} diff --git a/src/modules/toast/tests/clipboardErrorUtils.test.ts b/src/modules/toast/tests/clipboardErrorUtils.test.ts deleted file mode 100644 index 2971b0c3d..000000000 --- a/src/modules/toast/tests/clipboardErrorUtils.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { TOAST_MESSAGES } from '~/modules/toast/domain/toastMessages' -import { type ToastError } from '~/modules/toast/domain/toastTypes' -import { formatErrorForClipboard } from '~/modules/toast/infrastructure/clipboardErrorUtils' - -describe('formatErrorForClipboard', () => { - it('formats all fields correctly', () => { - const error: ToastError = { - message: 'Test error', - fullError: 'Full stacktrace', - context: { foo: 'bar', num: 42 }, - stack: 'stacktrace here', - timestamp: 1717600000000, - } - const result = formatErrorForClipboard(error) - expect(result).toContain(`${TOAST_MESSAGES.ERROR_TITLE}: Test error`) - expect(result).toContain(`${TOAST_MESSAGES.SHOW_DETAILS}: Full stacktrace`) - expect(result).toContain(`${TOAST_MESSAGES.COPY_ERROR}:`) - expect(result).toContain('foo') - expect(result).toContain('num') - expect(result).toContain('Stack Trace:') - expect(result).toContain('stacktrace here') - expect(result).toContain('Timestamp: 2024-06-05T') - expect(result).toContain('Error Report - ') - }) - - it('omits empty or duplicate fields', () => { - const error: ToastError = { - message: 'Error', - fullError: 'Error', - context: {}, - stack: '', - timestamp: 0, - } - const result = formatErrorForClipboard(error) - expect(result).toContain(`${TOAST_MESSAGES.ERROR_TITLE}: Error`) - expect(result).not.toContain(`${TOAST_MESSAGES.SHOW_DETAILS}:`) - expect(result).not.toContain(`${TOAST_MESSAGES.COPY_ERROR}:`) - expect(result).not.toContain('Stack Trace:') - expect(result).not.toContain('Timestamp:') - }) - - it('handles missing fields gracefully', () => { - const error = { - message: '', - fullError: '', - context: undefined, - stack: undefined, - timestamp: undefined, - } - const result = formatErrorForClipboard(error) - expect(result).toContain('Error Report - ') - expect(result).not.toContain('Message:') - expect(result).not.toContain('Details:') - expect(result).not.toContain('Context:') - expect(result).not.toContain('Stack Trace:') - expect(result).not.toContain('Timestamp:') - }) - - it('handles only message', () => { - const error: ToastError = { - message: 'Message only', - fullError: '', - context: {}, - stack: '', - timestamp: 0, - } - const result = formatErrorForClipboard(error) - expect(result).toContain(`${TOAST_MESSAGES.ERROR_TITLE}: Message only`) - expect(result).not.toContain(`${TOAST_MESSAGES.SHOW_DETAILS}:`) - expect(result).not.toContain(`${TOAST_MESSAGES.COPY_ERROR}:`) - }) - - it('handles only stack', () => { - const error: ToastError = { - message: '', - fullError: '', - context: {}, - stack: 'stack only', - timestamp: 0, - } - const result = formatErrorForClipboard(error) - expect(result).toContain('Stack Trace:') - expect(result).toContain('stack only') - expect(result).not.toContain('Message:') - expect(result).not.toContain('Details:') - expect(result).not.toContain('Context:') - expect(result).not.toContain('Timestamp:') - }) - - it('handles only context', () => { - const error: ToastError = { - message: '', - fullError: '', - context: { foo: 'bar' }, - stack: '', - timestamp: 0, - } - const result = formatErrorForClipboard(error) - expect(result).toContain(`${TOAST_MESSAGES.COPY_ERROR}:`) - expect(result).toContain('foo') - expect(result).not.toContain(`${TOAST_MESSAGES.ERROR_TITLE}:`) - }) - - it('handles only timestamp', () => { - const error: ToastError = { - message: '', - fullError: '', - context: {}, - stack: '', - timestamp: 1717600000000, - } - const result = formatErrorForClipboard(error) - expect(result).toContain('Timestamp: 2024-06-05T') - expect(result).not.toContain('Message:') - expect(result).not.toContain('Details:') - expect(result).not.toContain('Context:') - expect(result).not.toContain('Stack Trace:') - }) - - it('handles long message and multiline stack', () => { - const error: ToastError = { - message: 'Long message'.repeat(10), - fullError: '', - context: {}, - stack: 'line1\nline2\nline3', - timestamp: 0, - } - const result = formatErrorForClipboard(error) - expect(result).toContain('Long messageLong messageLong message') - expect(result).toContain('Stack Trace:') - expect(result).toContain('line1') - expect(result).toContain('line2') - expect(result).toContain('line3') - }) -}) diff --git a/src/modules/toast/ui/ExpandableErrorToast.tsx b/src/modules/toast/ui/ExpandableErrorToast.tsx index a76f57c69..885a39e50 100644 --- a/src/modules/toast/ui/ExpandableErrorToast.tsx +++ b/src/modules/toast/ui/ExpandableErrorToast.tsx @@ -14,7 +14,6 @@ import { type ToastItem, type ToastType, } from '~/modules/toast/domain/toastTypes' -import { handleCopyErrorToClipboard } from '~/modules/toast/infrastructure/clipboardErrorUtils' import { modalManager } from '~/shared/modal/core/modalManager' /** @@ -47,7 +46,6 @@ type ExpandableToastContentProps = { isTruncated: boolean originalMessage: string canExpand: boolean - onCopy: () => void errorDetails: ToastError type: ToastType } @@ -64,12 +62,6 @@ export function ExpandableToast(props: ExpandableToastProps) { props.onDismiss() } } - const handleCopy = () => { - void handleCopyErrorToClipboard(props.errorDetails, { - component: 'ExpandableErrorToast', - operation: 'handleCopy', - }) - } return (
@@ -329,14 +320,6 @@ function ExpandableToastContent(props: ExpandableToastContentProps) { > {TOAST_MESSAGES.SHOW_DETAILS} -
) diff --git a/src/sections/common/components/ErrorDetailModal.tsx b/src/sections/common/components/ErrorDetailModal.tsx deleted file mode 100644 index 97a90cc14..000000000 --- a/src/sections/common/components/ErrorDetailModal.tsx +++ /dev/null @@ -1,230 +0,0 @@ -/** - * Error Detail Modal Component - * - * A modal component that displays detailed error information, - * allowing users to view full stack traces and copy error details. - */ - -import { createSignal, Show } from 'solid-js' -import { Portal } from 'solid-js/web' - -import { type ToastError } from '~/modules/toast/domain/toastTypes' -import { handleCopyErrorToClipboard } from '~/modules/toast/infrastructure/clipboardErrorUtils' - -export type ErrorDetailModalProps = { - /** The error details to display */ - errorDetails: ToastError - /** Whether the modal is open */ - isOpen: boolean - /** Callback when the modal is closed */ - onClose: () => void -} - -/** - * Modal component for displaying detailed error information - */ -export function ErrorDetailModal(props: ErrorDetailModalProps) { - const [isCopied, setIsCopied] = createSignal(false) - - const handleCopy = async () => { - await handleCopyErrorToClipboard(props.errorDetails, { - component: 'ErrorDetailModal', - operation: 'handleCopy', - }) - setIsCopied(true) - setTimeout(() => setIsCopied(false), 2000) - } - - return ( - - -