From df184bd9dc9a6ae812949cd875faf5d0d13ca79d Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 18 Jun 2025 15:42:37 -0500 Subject: [PATCH 01/33] WIP add series entry button --- .../js_src/lib/components/Forms/Save.tsx | 81 +++++++++++++++++++ .../frontend/js_src/lib/localization/forms.ts | 12 +++ 2 files changed, 93 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 12ac816a4c9..3f633ad5031 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -242,10 +242,91 @@ export function SaveButton({ !tableValidForBulkClone(resource.specifyTable, resource) || formatter === undefined; + const showSeriesEntry = true; + const [seriesEntryStart, setSeriesEntryStart] = React.useState(undefined); + const [seriesEntryEnd, setSeriesEntryEnd] = React.useState(undefined); + return ( <> {typeof handleAdd === 'function' && canCreate ? ( <> + {resource.specifyTable.name === 'CollectionObject' && + (isInRecordSet === false || isInRecordSet === undefined) && + showSeriesEntry && canSave && !isCOGorCOJO + ? + <> + + setSeriesEntryStart(value) + } + /> + + setSeriesEntryEnd(value) + } + /> + { + // Scroll to the top of the form on clone + smoothScroll(form, 0); + const handleClick = async (): Promise>> => { + // Simple test, don't generate cns between start and end + const catalogNumbers = [seriesEntryStart, seriesEntryEnd]; + + const clonePromises = Array.from( + { length: catalogNumbers.length }, + async (_, index) => { + const clonedResource = await resource.clone( + false, + true + ); + clonedResource.set( + 'catalogNumber', + catalogNumbers[index] as never + ); + return clonedResource; + } + ); + + const clones = await Promise.all(clonePromises); + + const backendClones = await ajax< + RA> + >( + `/api/specify/bulk/${resource.specifyTable.name.toLowerCase()}/`, + { + method: 'POST', + headers: { Accept: 'application/json' }, + body: clones, + } + ).then(({ data }) => + data.map((resource) => + deserializeResource(serializeResource(resource)) + ) + ); + + return Promise.all([resource, ...backendClones]); + } + loading(handleClick().then(handleAdd)); + }} + > + {formsText.seriesEntry()} + + + : undefined} {resource.specifyTable.name === 'CollectionObject' && (isInRecordSet === false || isInRecordSet === undefined) && isSaveDisabled && diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index fc842be7553..1f5755ddf03 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1197,4 +1197,16 @@ export const formsText = createDictionary({ "ru-ru": "Добавить детей COG", "uk-ua": "Додати дочірні елементи COG", }, + seriesEntry: { + "en-us": "Series Entry", + }, + seriesEntryDescription: { + "en-us": "Create a series of new records from a range of catalog numbers", + }, + seriesEntryStart: { + "en-us": "Series Range Start", + }, + seriesEntryEnd: { + "en-us": "Series Range End", + }, } as const); From 8e66d3e7b0de6f7a80ebe1678fc8c81fe755354f Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 18 Jun 2025 20:46:39 +0000 Subject: [PATCH 02/33] Lint code with ESLint and Prettier Triggered by df184bd9dc9a6ae812949cd875faf5d0d13ca79d on branch refs/heads/issue-6276 --- .../js_src/lib/components/Forms/Save.tsx | 4 +- .../frontend/js_src/lib/localization/forms.ts | 1838 ++++++++--------- 2 files changed, 921 insertions(+), 921 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 3f633ad5031..a7e58dd8288 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -258,9 +258,9 @@ export function SaveButton({ setSeriesEntryStart(value) } @@ -268,9 +268,9 @@ export function SaveButton({ setSeriesEntryEnd(value) } diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 1f5755ddf03..a489f7d0794 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -5,1208 +5,1208 @@ * @module */ -import { createDictionary } from "./utils"; +import { createDictionary } from './utils'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file export const formsText = createDictionary({ forms: { - "en-us": "Forms", - "ru-ru": "Формы", - "es-es": "Formularios", - "fr-fr": "Formulaires", - "uk-ua": "Форми", - "de-ch": "Formulare", - "pt-br": "Formulários", + 'en-us': 'Forms', + 'ru-ru': 'Формы', + 'es-es': 'Formularios', + 'fr-fr': 'Formulaires', + 'uk-ua': 'Форми', + 'de-ch': 'Formulare', + 'pt-br': 'Formulários', }, clone: { - "en-us": "Clone", - "ru-ru": "Клон", - "es-es": "Clon", - "fr-fr": "Cloner", - "uk-ua": "Клон", - "de-ch": "Klone", - "pt-br": "Clone", + 'en-us': 'Clone', + 'ru-ru': 'Клон', + 'es-es': 'Clon', + 'fr-fr': 'Cloner', + 'uk-ua': 'Клон', + 'de-ch': 'Klone', + 'pt-br': 'Clone', }, cloneDescription: { - "en-us": "Create a full copy of current record", - "ru-ru": "Создать полную копию текущей записи", - "es-es": "Crear una copia completa del registro actual", - "fr-fr": "Créer une copie complète de l'enregistrement actuel", - "uk-ua": "Створити повну копію поточного запису", - "de-ch": "Erstellen einer kompletten Kopie des aktuellen Datensatzes", - "pt-br": "Crie uma cópia completa do registro atual", + 'en-us': 'Create a full copy of current record', + 'ru-ru': 'Создать полную копию текущей записи', + 'es-es': 'Crear una copia completa del registro actual', + 'fr-fr': "Créer une copie complète de l'enregistrement actuel", + 'uk-ua': 'Створити повну копію поточного запису', + 'de-ch': 'Erstellen einer kompletten Kopie des aktuellen Datensatzes', + 'pt-br': 'Crie uma cópia completa do registro atual', }, valueMustBeUniqueToField: { - "en-us": "Value must be unique to {fieldName:string}", - "ru-ru": "Значение должно быть уникальным для {fieldName:string}", - "es-es": "El valor debe ser exclusivo de {fieldName:string}", - "fr-fr": "La valeur doit être unique à {fieldName:string}", - "uk-ua": "Значення має бути унікальним для {fieldName:string}", - "de-ch": "Der Wert muss für {fieldName:string} eindeutig sein", - "pt-br": "O valor deve ser exclusivo para {fieldName:string}", + 'en-us': 'Value must be unique to {fieldName:string}', + 'ru-ru': 'Значение должно быть уникальным для {fieldName:string}', + 'es-es': 'El valor debe ser exclusivo de {fieldName:string}', + 'fr-fr': 'La valeur doit être unique à {fieldName:string}', + 'uk-ua': 'Значення має бути унікальним для {fieldName:string}', + 'de-ch': 'Der Wert muss für {fieldName:string} eindeutig sein', + 'pt-br': 'O valor deve ser exclusivo para {fieldName:string}', }, valueMustBeUniqueToDatabase: { - "en-us": "Value must be unique to database", - "ru-ru": "Значение должно быть уникальным в базе данных.", - "es-es": "El valor debe ser exclusivo de la base de datos.", - "fr-fr": "La valeur doit être unique dans la base de données", - "uk-ua": "Значення має бути унікальним для бази даних", - "de-ch": "Der Wert muss in der Datenbank eindeutig sein", - "pt-br": "O valor deve ser exclusivo para o banco de dados", + 'en-us': 'Value must be unique to database', + 'ru-ru': 'Значение должно быть уникальным в базе данных.', + 'es-es': 'El valor debe ser exclusivo de la base de datos.', + 'fr-fr': 'La valeur doit être unique dans la base de données', + 'uk-ua': 'Значення має бути унікальним для бази даних', + 'de-ch': 'Der Wert muss in der Datenbank eindeutig sein', + 'pt-br': 'O valor deve ser exclusivo para o banco de dados', }, valuesOfMustBeUniqueToField: { - "en-us": "Values of {values:string} must be unique to {fieldName:string}", - "ru-ru": - "Значения {values:string} должны быть уникальными для {fieldName:string}", - "es-es": - "Los valores de {values:string} deben ser únicos para {fieldName:string}", - "fr-fr": - "Les valeurs de {values:string} doivent être uniques à {fieldName:string}", - "uk-ua": - "Значення {values:string} мають бути унікальними для {fieldName:string}", - "de-ch": - "Werte von {values:string} müssen für {fieldName:string} eindeutig sein.", - "pt-br": - "Os valores de {values:string} devem ser exclusivos de {fieldName:string}", + 'en-us': 'Values of {values:string} must be unique to {fieldName:string}', + 'ru-ru': + 'Значения {values:string} должны быть уникальными для {fieldName:string}', + 'es-es': + 'Los valores de {values:string} deben ser únicos para {fieldName:string}', + 'fr-fr': + 'Les valeurs de {values:string} doivent être uniques à {fieldName:string}', + 'uk-ua': + 'Значення {values:string} мають бути унікальними для {fieldName:string}', + 'de-ch': + 'Werte von {values:string} müssen für {fieldName:string} eindeutig sein.', + 'pt-br': + 'Os valores de {values:string} devem ser exclusivos de {fieldName:string}', }, valuesOfMustBeUniqueToDatabase: { - "en-us": "Values of {values:string} must be unique to database", - "ru-ru": "Значения {values:string} должны быть уникальными в базе данных.", - "es-es": - "Los valores de {values:string} deben ser únicos para la base de datos.", - "fr-fr": - "Les valeurs de {values:string} doivent être uniques à la base de données", - "uk-ua": "Значення {values:string} мають бути унікальними для бази даних", - "de-ch": "Werte von {values:string} müssen in der Datenbank eindeutig sein", - "pt-br": - "Os valores de {values:string} devem ser exclusivos do banco de dados", + 'en-us': 'Values of {values:string} must be unique to database', + 'ru-ru': 'Значения {values:string} должны быть уникальными в базе данных.', + 'es-es': + 'Los valores de {values:string} deben ser únicos para la base de datos.', + 'fr-fr': + 'Les valeurs de {values:string} doivent être uniques à la base de données', + 'uk-ua': 'Значення {values:string} мають бути унікальними для бази даних', + 'de-ch': 'Werte von {values:string} müssen in der Datenbank eindeutig sein', + 'pt-br': + 'Os valores de {values:string} devem ser exclusivos do banco de dados', }, checkingIfResourceCanBeDeleted: { - "en-us": "Checking if resource can be deleted…", - "ru-ru": "Проверка возможности удаления ресурса…", - "es-es": "Comprobando si el recurso se puede eliminar…", - "fr-fr": "Vérification si la ressource peut être supprimée…", - "uk-ua": "Перевірка можливості видалення ресурсу…", - "de-ch": "Überprüfen, ob die Ressource gelöscht werden kann …", - "pt-br": "Verificando se o recurso pode ser excluído…", + 'en-us': 'Checking if resource can be deleted…', + 'ru-ru': 'Проверка возможности удаления ресурса…', + 'es-es': 'Comprobando si el recurso se puede eliminar…', + 'fr-fr': 'Vérification si la ressource peut être supprimée…', + 'uk-ua': 'Перевірка можливості видалення ресурсу…', + 'de-ch': 'Überprüfen, ob die Ressource gelöscht werden kann …', + 'pt-br': 'Verificando se o recurso pode ser excluído…', }, deleteBlocked: { - "en-us": "Delete blocked", - "ru-ru": "Удалить заблокированный", - "es-es": "Eliminar bloqueado", - "fr-fr": "Supprimer bloqué", - "uk-ua": "Видалити заблоковано", - "de-ch": "Gesperrte löschen", - "pt-br": "Excluir bloqueado", + 'en-us': 'Delete blocked', + 'ru-ru': 'Удалить заблокированный', + 'es-es': 'Eliminar bloqueado', + 'fr-fr': 'Supprimer bloqué', + 'uk-ua': 'Видалити заблоковано', + 'de-ch': 'Gesperrte löschen', + 'pt-br': 'Excluir bloqueado', }, deleteBlockedDescription: { - "en-us": - "The resource cannot be deleted because it is referenced by the following resources:", - "de-ch": - "Die Ressource kann nicht gelöscht werden, da sie von den folgenden Ressourcen referenziert wird:", - "es-es": "encontrar usos", - "fr-fr": - "La ressource ne peut pas être supprimée car elle est référencée par les ressources suivantes :", - "ru-ru": - "Ресурс не может быть удален, поскольку на него ссылаются следующие ресурсы:", - "uk-ua": - "Ресурс не можна видалити, оскільки на нього посилаються такі ресурси:", - "pt-br": - "O recurso não pode ser excluído porque é referenciado pelos seguintes recursos:", + 'en-us': + 'The resource cannot be deleted because it is referenced by the following resources:', + 'de-ch': + 'Die Ressource kann nicht gelöscht werden, da sie von den folgenden Ressourcen referenziert wird:', + 'es-es': 'encontrar usos', + 'fr-fr': + 'La ressource ne peut pas être supprimée car elle est référencée par les ressources suivantes :', + 'ru-ru': + 'Ресурс не может быть удален, поскольку на него ссылаются следующие ресурсы:', + 'uk-ua': + 'Ресурс не можна видалити, оскільки на нього посилаються такі ресурси:', + 'pt-br': + 'O recurso não pode ser excluído porque é referenciado pelos seguintes recursos:', }, relationship: { - "en-us": "Relationship", - "ru-ru": "Отношение", - "es-es": "Relación", - "fr-fr": "Relation", - "uk-ua": "Стосунків", - "de-ch": "Beziehung", - "pt-br": "Relação", + 'en-us': 'Relationship', + 'ru-ru': 'Отношение', + 'es-es': 'Relación', + 'fr-fr': 'Relation', + 'uk-ua': 'Стосунків', + 'de-ch': 'Beziehung', + 'pt-br': 'Relação', }, paleoMap: { - "en-us": "Paleo Map", - "ru-ru": "Палеокарта", - "es-es": "Mapa Paleo", - "fr-fr": "Carte paléo", - "uk-ua": "Палео-мапа", - "de-ch": "Paläo-Karte", - "pt-br": "Mapa Paleo", + 'en-us': 'Paleo Map', + 'ru-ru': 'Палеокарта', + 'es-es': 'Mapa Paleo', + 'fr-fr': 'Carte paléo', + 'uk-ua': 'Палео-мапа', + 'de-ch': 'Paläo-Karte', + 'pt-br': 'Mapa Paleo', }, paleoRequiresGeography: { - comment: "Example: Geography Required", - "en-us": "{geographyTable:string} Required", - "ru-ru": "{geographyTable:string} Требуется", - "es-es": "{geographyTable:string} Requerido", - "fr-fr": "{geographyTable:string} Obligatoire", - "uk-ua": "{geographyTable:string} Обов'язково", - "de-ch": "{geographyTable:string} Erforderlich", - "pt-br": "{geographyTable:string} Obrigatório", + comment: 'Example: Geography Required', + 'en-us': '{geographyTable:string} Required', + 'ru-ru': '{geographyTable:string} Требуется', + 'es-es': '{geographyTable:string} Requerido', + 'fr-fr': '{geographyTable:string} Obligatoire', + 'uk-ua': "{geographyTable:string} Обов'язково", + 'de-ch': '{geographyTable:string} Erforderlich', + 'pt-br': '{geographyTable:string} Obrigatório', }, paleoRequiresGeographyDescription: { - "en-us": - "The Paleo Map plugin requires that the {localityTable:string} have geographic coordinates and that the paleo context have a geographic age with at least a start time or and end time populated.", - "de-ch": - "Das Paleo Map-Plugin erfordert, dass {localityTable:string} geografische Koordinaten hat und dass der Paläo-Kontext ein geografisches Alter mit mindestens einer ausgefüllten Start- oder Endzeit hat.", - "es-es": "Seleccionar fuente de tablas", - "fr-fr": - "Le plugin Paleo Map nécessite que les {localityTable:string} aient des coordonnées géographiques et que le contexte paléo ait un âge géographique avec au moins une heure de début ou une heure de fin renseignée.", - "ru-ru": - "Плагин Paleo Map требует, чтобы {localityTable:string} имел географические координаты и чтобы палеоконтекст имел географический возраст с указанием как минимум начального или конечного времени.", - "uk-ua": - "Плагін Paleo Map вимагає, щоб {localityTable:string} мав географічні координати, а палеодієнтний контекст мав географічний вік із зазначенням принаймні часу початку або часу завершення.", - "pt-br": - "O plugin Paleo Map requer que o {localityTable:string} tenha coordenadas geográficas e que o contexto paleo tenha uma idade geográfica com pelo menos um horário de início ou término preenchidos.", + 'en-us': + 'The Paleo Map plugin requires that the {localityTable:string} have geographic coordinates and that the paleo context have a geographic age with at least a start time or and end time populated.', + 'de-ch': + 'Das Paleo Map-Plugin erfordert, dass {localityTable:string} geografische Koordinaten hat und dass der Paläo-Kontext ein geografisches Alter mit mindestens einer ausgefüllten Start- oder Endzeit hat.', + 'es-es': 'Seleccionar fuente de tablas', + 'fr-fr': + 'Le plugin Paleo Map nécessite que les {localityTable:string} aient des coordonnées géographiques et que le contexte paléo ait un âge géographique avec au moins une heure de début ou une heure de fin renseignée.', + 'ru-ru': + 'Плагин Paleo Map требует, чтобы {localityTable:string} имел географические координаты и чтобы палеоконтекст имел географический возраст с указанием как минимум начального или конечного времени.', + 'uk-ua': + 'Плагін Paleo Map вимагає, щоб {localityTable:string} мав географічні координати, а палеодієнтний контекст мав географічний вік із зазначенням принаймні часу початку або часу завершення.', + 'pt-br': + 'O plugin Paleo Map requer que o {localityTable:string} tenha coordenadas geográficas e que o contexto paleo tenha uma idade geográfica com pelo menos um horário de início ou término preenchidos.', }, invalidDate: { - "en-us": "Invalid Date", - "ru-ru": "Неверная дата", - "es-es": "Fecha invalida", - "fr-fr": "Date invalide", - "uk-ua": "Недійсна дата", - "de-ch": "Ungültiges Datum", - "pt-br": "Data inválida", + 'en-us': 'Invalid Date', + 'ru-ru': 'Неверная дата', + 'es-es': 'Fecha invalida', + 'fr-fr': 'Date invalide', + 'uk-ua': 'Недійсна дата', + 'de-ch': 'Ungültiges Datum', + 'pt-br': 'Data inválida', }, deleteConfirmation: { - "en-us": - "Are you sure you want to permanently delete this {tableName:string} from the database?", - "de-ch": - "Sind Sie sicher, dass Sie diesen {tableName:string} dauerhaft aus der Datenbank löschen möchten?", - "es-es": "El valor debe ser exclusivo de la base de datos.", - "fr-fr": - "Êtes-vous sûr de vouloir supprimer définitivement ce {tableName:string} de la base de données ?", - "ru-ru": - "Вы уверены, что хотите навсегда удалить {tableName:string} из базы данных?", - "uk-ua": - "Ви впевнені, що хочете остаточно видалити цей {tableName:string} з бази даних?", - "pt-br": - "Tem certeza de que deseja excluir permanentemente este {tableName:string} do banco de dados?", + 'en-us': + 'Are you sure you want to permanently delete this {tableName:string} from the database?', + 'de-ch': + 'Sind Sie sicher, dass Sie diesen {tableName:string} dauerhaft aus der Datenbank löschen möchten?', + 'es-es': 'El valor debe ser exclusivo de la base de datos.', + 'fr-fr': + 'Êtes-vous sûr de vouloir supprimer définitivement ce {tableName:string} de la base de données ?', + 'ru-ru': + 'Вы уверены, что хотите навсегда удалить {tableName:string} из базы данных?', + 'uk-ua': + 'Ви впевнені, що хочете остаточно видалити цей {tableName:string} з бази даних?', + 'pt-br': + 'Tem certeza de que deseja excluir permanentemente este {tableName:string} do banco de dados?', }, deleteConfirmationDescription: { - "en-us": "This action cannot be undone.", - "ru-ru": "Это действие не может быть отменено.", - "es-es": "Esta acción no se puede deshacer.", - "fr-fr": "Cette action ne peut pas être annulée.", - "uk-ua": "Цю дію не можна скасувати.", - "de-ch": "Diese Aktion kann nicht rückgängig gemacht werden.", - "pt-br": "Esta ação não pode ser desfeita.", + 'en-us': 'This action cannot be undone.', + 'ru-ru': 'Это действие не может быть отменено.', + 'es-es': 'Esta acción no se puede deshacer.', + 'fr-fr': 'Cette action ne peut pas être annulée.', + 'uk-ua': 'Цю дію не можна скасувати.', + 'de-ch': 'Diese Aktion kann nicht rückgängig gemacht werden.', + 'pt-br': 'Esta ação não pode ser desfeita.', }, datePrecision: { - "en-us": "Date Precision", - "ru-ru": "Точность даты", - "es-es": "Precisión de fecha", - "fr-fr": "Précision de la date", - "uk-ua": "Точність дати", - "de-ch": "Datumsgenauigkeit", - "pt-br": "Precisão de data", + 'en-us': 'Date Precision', + 'ru-ru': 'Точность даты', + 'es-es': 'Precisión de fecha', + 'fr-fr': 'Précision de la date', + 'uk-ua': 'Точність дати', + 'de-ch': 'Datumsgenauigkeit', + 'pt-br': 'Precisão de data', }, monthYear: { comment: ` A placeholder for partial date field when "month /year" type is selected. Visible only in browsers that don\'t support the "month" input type. `, - "en-us": "Mon / Year", - "ru-ru": "Пн / Год", - "es-es": "Usar configuraciones personalizadas", - "fr-fr": "Lun / Année", - "uk-ua": "Пн / Рік", - "de-ch": "Mo / Jahr", - "pt-br": "Seg / Ano", + 'en-us': 'Mon / Year', + 'ru-ru': 'Пн / Год', + 'es-es': 'Usar configuraciones personalizadas', + 'fr-fr': 'Lun / Année', + 'uk-ua': 'Пн / Рік', + 'de-ch': 'Mo / Jahr', + 'pt-br': 'Seg / Ano', }, yearPlaceholder: { comment: 'A placeholder for partial date field when "year" type is selected', - "en-us": "YYYY", - "ru-ru": "ГГГГ", - "es-es": "AAAA", - "fr-fr": "AAAA", - "uk-ua": "РРРР", - "de-ch": "JJJJ", - "pt-br": "AAAA", + 'en-us': 'YYYY', + 'ru-ru': 'ГГГГ', + 'es-es': 'AAAA', + 'fr-fr': 'AAAA', + 'uk-ua': 'РРРР', + 'de-ch': 'JJJJ', + 'pt-br': 'AAAA', }, today: { - "en-us": "Today", - "ru-ru": "Сегодня", - "es-es": "Hoy", - "fr-fr": "Aujourd'hui", - "uk-ua": "Сьогодні", - "de-ch": "Heute", - "pt-br": "Hoje", + 'en-us': 'Today', + 'ru-ru': 'Сегодня', + 'es-es': 'Hoy', + 'fr-fr': "Aujourd'hui", + 'uk-ua': 'Сьогодні', + 'de-ch': 'Heute', + 'pt-br': 'Hoje', }, todayButtonDescription: { - "en-us": "Set to current date", - "ru-ru": "Установить на текущую дату", - "es-es": "Establecer en fecha actual", - "fr-fr": "Définir sur la date du jour", - "uk-ua": "Встановити на поточну дату", - "de-ch": "Auf aktuelles Datum einstellen", - "pt-br": "Definir para a data atual", + 'en-us': 'Set to current date', + 'ru-ru': 'Установить на текущую дату', + 'es-es': 'Establecer en fecha actual', + 'fr-fr': 'Définir sur la date du jour', + 'uk-ua': 'Встановити на поточну дату', + 'de-ch': 'Auf aktuelles Datum einstellen', + 'pt-br': 'Definir para a data atual', }, addToPickListConfirmation: { - "en-us": "Add to {pickListTable:string}?", - "ru-ru": "Добавить в {pickListTable:string}?", - "es-es": "¿Añadir a {pickListTable:string}?", - "fr-fr": "Ajouter à {pickListTable:string} ?", - "uk-ua": "Додати до {pickListTable:string}?", - "de-ch": "Zu {pickListTable:string} hinzufügen?", - "pt-br": "Adicionar a {pickListTable:string}?", + 'en-us': 'Add to {pickListTable:string}?', + 'ru-ru': 'Добавить в {pickListTable:string}?', + 'es-es': '¿Añadir a {pickListTable:string}?', + 'fr-fr': 'Ajouter à {pickListTable:string} ?', + 'uk-ua': 'Додати до {pickListTable:string}?', + 'de-ch': 'Zu {pickListTable:string} hinzufügen?', + 'pt-br': 'Adicionar a {pickListTable:string}?', }, addToPickListConfirmationDescription: { - "en-us": + 'en-us': 'Add value "{value:string}" to the {pickListTable:string} named "{pickListName:string}"?', - "de-ch": - "Wert „{value:string}“ zum {pickListTable:string} mit dem Namen „{pickListName:string}“ hinzufügen?", - "es-es": + 'de-ch': + 'Wert „{value:string}“ zum {pickListTable:string} mit dem Namen „{pickListName:string}“ hinzufügen?', + 'es-es': '¿Agregar valor "{value:string}" al {pickListTable:string} llamado "{pickListName:string}"?', - "fr-fr": - "Ajouter la valeur « {value:string} » au {pickListTable:string} nommé « {pickListName:string} » ?", - "ru-ru": - "Добавить значение «{value:string}» к {pickListTable:string} с именем «{pickListName:string}»?", - "uk-ua": + 'fr-fr': + 'Ajouter la valeur « {value:string} » au {pickListTable:string} nommé « {pickListName:string} » ?', + 'ru-ru': + 'Добавить значение «{value:string}» к {pickListTable:string} с именем «{pickListName:string}»?', + 'uk-ua': 'Додати значення "{value:string}" до {pickListTable:string} з назвою "{pickListName:string}"?', - "pt-br": + 'pt-br': 'Adicionar valor "{value:string}" ao {pickListTable:string} chamado "{pickListName:string}"?', }, invalidType: { - "en-us": "Invalid Type", - "ru-ru": "Неверный тип", - "es-es": "Tipo inválido", - "fr-fr": "Type invalide", - "uk-ua": "Недійсний тип", - "de-ch": "Ungültiger Typ", - "pt-br": "Tipo inválido", + 'en-us': 'Invalid Type', + 'ru-ru': 'Неверный тип', + 'es-es': 'Tipo inválido', + 'fr-fr': 'Type invalide', + 'uk-ua': 'Недійсний тип', + 'de-ch': 'Ungültiger Typ', + 'pt-br': 'Tipo inválido', }, invalidNumericPicklistValue: { - "en-us": "Only numeric values are supported in this {pickListTable:string}", - "de-ch": - "Es werden nur numerische Werte unterstützt {pickListTable:string}", - "es-es": "En este {pickListTable:string} solo se admiten valores numéricos", - "fr-fr": - "Seules les valeurs numériques sont prises en charge dans ce {pickListTable:string}", - "ru-ru": - "В этом {pickListTable:string} поддерживаются только числовые значения.", - "uk-ua": - "У цьому {pickListTable:string} підтримуються лише числові значення", - "pt-br": - "Somente valores numéricos são suportados neste {pickListTable:string}", + 'en-us': 'Only numeric values are supported in this {pickListTable:string}', + 'de-ch': + 'Es werden nur numerische Werte unterstützt {pickListTable:string}', + 'es-es': 'En este {pickListTable:string} solo se admiten valores numéricos', + 'fr-fr': + 'Seules les valeurs numériques sont prises en charge dans ce {pickListTable:string}', + 'ru-ru': + 'В этом {pickListTable:string} поддерживаются только числовые значения.', + 'uk-ua': + 'У цьому {pickListTable:string} підтримуються лише числові значення', + 'pt-br': + 'Somente valores numéricos são suportados neste {pickListTable:string}', }, noData: { - "en-us": "No Data.", - "ru-ru": "Нет данных.", - "es-es": "Sin datos.", - "fr-fr": "Aucune donnée.", - "uk-ua": "Немає даних.", - "de-ch": "Keine Daten.", - "pt-br": "Nenhum dado.", + 'en-us': 'No Data.', + 'ru-ru': 'Нет данных.', + 'es-es': 'Sin datos.', + 'fr-fr': 'Aucune donnée.', + 'uk-ua': 'Немає даних.', + 'de-ch': 'Keine Daten.', + 'pt-br': 'Nenhum dado.', }, recordSetDeletionWarning: { - "en-us": + 'en-us': 'The {recordSetTable:string} "{recordSetName:string}" will be deleted. The referenced records will NOT be deleted from the database.', - "ru-ru": + 'ru-ru': '{recordSetTable:string} "{recordSetName:string}" будет удалено. Связанные записи НЕ будут удалены из базы данных.', - "es-es": + 'es-es': 'Se eliminará el {recordSetTable:string} "{recordSetName:string}". Los registros referenciados no se eliminarán de la base de datos.', - "fr-fr": - "Le {recordSetTable:string} « {recordSetName:string} » sera supprimé. Les enregistrements référencés ne seront PAS supprimés de la base de données.", - "uk-ua": + 'fr-fr': + 'Le {recordSetTable:string} « {recordSetName:string} » sera supprimé. Les enregistrements référencés ne seront PAS supprimés de la base de données.', + 'uk-ua': '{recordSetTable:string} "{recordSetName:string}" буде видалено. Записи, на які посилаються, НЕ будуть видалені з бази даних.', - "de-ch": + 'de-ch': 'Der {recordSetTable:string} "{recordSetName:string}" wird gelöscht. Die referenzierten Datensätze werden NICHT aus der Datenbank gelöscht.', - "pt-br": + 'pt-br': 'O {recordSetTable:string} "{recordSetName:string}" será excluído. Os registros referenciados NÃO serão excluídos do banco de dados.', }, saveRecordFirst: { - "en-us": "Save record first", - "ru-ru": "Сначала сохраните запись", - "es-es": "Guardar el registro primero", - "fr-fr": "Enregistrer d'abord l'enregistrement", - "uk-ua": "Спочатку збережіть запис", - "de-ch": "Datensatz zuerst speichern", - "pt-br": "Salvar registro primeiro", + 'en-us': 'Save record first', + 'ru-ru': 'Сначала сохраните запись', + 'es-es': 'Guardar el registro primero', + 'fr-fr': "Enregistrer d'abord l'enregistrement", + 'uk-ua': 'Спочатку збережіть запис', + 'de-ch': 'Datensatz zuerst speichern', + 'pt-br': 'Salvar registro primeiro', }, firstRecord: { - "en-us": "First Record", - "ru-ru": "Первая запись", - "es-es": "Primer disco", - "fr-fr": "Premier enregistrement", - "uk-ua": "Перший запис", - "de-ch": "Erster Eintrag", - "pt-br": "Primeiro Registro", + 'en-us': 'First Record', + 'ru-ru': 'Первая запись', + 'es-es': 'Primer disco', + 'fr-fr': 'Premier enregistrement', + 'uk-ua': 'Перший запис', + 'de-ch': 'Erster Eintrag', + 'pt-br': 'Primeiro Registro', }, lastRecord: { - "en-us": "Last Record", - "ru-ru": "Последняя запись", - "es-es": "Último registro", - "fr-fr": "Dernier enregistrement", - "uk-ua": "Останній запис", - "de-ch": "Letzter Datensatz", - "pt-br": "Último registro", + 'en-us': 'Last Record', + 'ru-ru': 'Последняя запись', + 'es-es': 'Último registro', + 'fr-fr': 'Dernier enregistrement', + 'uk-ua': 'Останній запис', + 'de-ch': 'Letzter Datensatz', + 'pt-br': 'Último registro', }, previousRecord: { - "en-us": "Previous Record", - "ru-ru": "Предыдущая запись", - "es-es": "Registro anterior", - "fr-fr": "Record précédent", - "uk-ua": "Попередній запис", - "de-ch": "Vorheriger Datensatz", - "pt-br": "Registro anterior", + 'en-us': 'Previous Record', + 'ru-ru': 'Предыдущая запись', + 'es-es': 'Registro anterior', + 'fr-fr': 'Record précédent', + 'uk-ua': 'Попередній запис', + 'de-ch': 'Vorheriger Datensatz', + 'pt-br': 'Registro anterior', }, nextRecord: { - "en-us": "Next Record", - "ru-ru": "Следующая запись", - "es-es": "Próximo récord", - "fr-fr": "Prochain enregistrement", - "uk-ua": "Наступний запис", - "de-ch": "Nächster Datensatz", - "pt-br": "Próximo registro", + 'en-us': 'Next Record', + 'ru-ru': 'Следующая запись', + 'es-es': 'Próximo récord', + 'fr-fr': 'Prochain enregistrement', + 'uk-ua': 'Наступний запис', + 'de-ch': 'Nächster Datensatz', + 'pt-br': 'Próximo registro', }, currentRecord: { - "en-us": "Current object (out of {total:number|formatted})", - "ru-ru": "Текущий объект (из {total:number|formatted})", - "es-es": "Objeto actual (de {total:number|formatted})", - "fr-fr": "Objet actuel (sur {total:number|formatted})", - "uk-ua": "Поточний об'єкт (з {total:number|formatted})", - "de-ch": "Aktuelles Objekt (aus {total:number|formatted})", - "pt-br": "Objeto atual (de {total:number|formatted})", + 'en-us': 'Current object (out of {total:number|formatted})', + 'ru-ru': 'Текущий объект (из {total:number|formatted})', + 'es-es': 'Objeto actual (de {total:number|formatted})', + 'fr-fr': 'Objet actuel (sur {total:number|formatted})', + 'uk-ua': "Поточний об'єкт (з {total:number|formatted})", + 'de-ch': 'Aktuelles Objekt (aus {total:number|formatted})', + 'pt-br': 'Objeto atual (de {total:number|formatted})', }, unsavedFormUnloadProtect: { - "en-us": "This form has not been saved.", - "ru-ru": "Эта форма не была сохранена.", - "es-es": "Este formulario no ha sido guardado.", - "fr-fr": "Ce formulaire n'a pas été enregistré.", - "uk-ua": "Цю форму не збережено.", - "de-ch": "Dieses Formular wurde nicht gespeichert.", - "pt-br": "Este formulário não foi salvo.", + 'en-us': 'This form has not been saved.', + 'ru-ru': 'Эта форма не была сохранена.', + 'es-es': 'Este formulario no ha sido guardado.', + 'fr-fr': "Ce formulaire n'a pas été enregistré.", + 'uk-ua': 'Цю форму не збережено.', + 'de-ch': 'Dieses Formular wurde nicht gespeichert.', + 'pt-br': 'Este formulário não foi salvo.', }, saveConflict: { - comment: "Meaning a conflict occurred when saving", - "en-us": "Save conflict", - "ru-ru": "Сохранить конфликт", - "es-es": "Guardar conflicto", - "fr-fr": "Enregistrer le conflit", - "uk-ua": "Зберегти конфлікт", - "de-ch": "Konflikt speichern", - "pt-br": "Salvar conflito", + comment: 'Meaning a conflict occurred when saving', + 'en-us': 'Save conflict', + 'ru-ru': 'Сохранить конфликт', + 'es-es': 'Guardar conflicto', + 'fr-fr': 'Enregistrer le conflit', + 'uk-ua': 'Зберегти конфлікт', + 'de-ch': 'Konflikt speichern', + 'pt-br': 'Salvar conflito', }, saveConflictDescription: { - "en-us": - "The data shown on this page has been changed by another user or in another browser tab and is out of date. The page must be reloaded to prevent inconsistent data from being saved.", - "ru-ru": - "Данные, отображаемые на этой странице, были изменены другим пользователем или на другой вкладке браузера и устарели. Страницу необходимо перезагрузить, чтобы предотвратить сохранение несогласованных данных.", - "es-es": - "Los datos que se muestran en esta página han sido modificados por otro usuario o en otra pestaña del navegador y están desactualizados. Es necesario recargar la página para evitar que se guarden datos incoherentes.", - "fr-fr": + 'en-us': + 'The data shown on this page has been changed by another user or in another browser tab and is out of date. The page must be reloaded to prevent inconsistent data from being saved.', + 'ru-ru': + 'Данные, отображаемые на этой странице, были изменены другим пользователем или на другой вкладке браузера и устарели. Страницу необходимо перезагрузить, чтобы предотвратить сохранение несогласованных данных.', + 'es-es': + 'Los datos que se muestran en esta página han sido modificados por otro usuario o en otra pestaña del navegador y están desactualizados. Es necesario recargar la página para evitar que se guarden datos incoherentes.', + 'fr-fr': "Les données affichées sur cette page ont été modifiées par un autre utilisateur ou dans un autre onglet du navigateur et sont obsolètes. La page doit être rechargée pour éviter l'enregistrement de données incohérentes.", - "uk-ua": - "Дані, що відображаються на цій сторінці, були змінені іншим користувачем або в іншій вкладці браузера та застарілі. Сторінку необхідно перезавантажити, щоб запобігти збереженню невідповідних даних.", - "de-ch": - "Die auf dieser Seite angezeigten Daten wurden von einem anderen Benutzer oder in einem anderen Browser-Tab geändert und sind veraltet. Um die Speicherung inkonsistenter Daten zu verhindern, muss die Seite neu geladen werden.", - "pt-br": - "Os dados exibidos nesta página foram alterados por outro usuário ou em outra aba do navegador e estão desatualizados. A página deve ser recarregada para evitar que dados inconsistentes sejam salvos.", + 'uk-ua': + 'Дані, що відображаються на цій сторінці, були змінені іншим користувачем або в іншій вкладці браузера та застарілі. Сторінку необхідно перезавантажити, щоб запобігти збереженню невідповідних даних.', + 'de-ch': + 'Die auf dieser Seite angezeigten Daten wurden von einem anderen Benutzer oder in einem anderen Browser-Tab geändert und sind veraltet. Um die Speicherung inkonsistenter Daten zu verhindern, muss die Seite neu geladen werden.', + 'pt-br': + 'Os dados exibidos nesta página foram alterados por outro usuário ou em outra aba do navegador e estão desatualizados. A página deve ser recarregada para evitar que dados inconsistentes sejam salvos.', }, saveBlocked: { - "en-us": "Save blocked", - "de-ch": "Speichern blockiert", - "es-es": "Guardar bloqueado", - "fr-fr": "Enregistrer bloqué", - "ru-ru": "Сохранить заблокировано", - "uk-ua": "Зберегти заблоковано", - "pt-br": "Salvar bloqueado", + 'en-us': 'Save blocked', + 'de-ch': 'Speichern blockiert', + 'es-es': 'Guardar bloqueado', + 'fr-fr': 'Enregistrer bloqué', + 'ru-ru': 'Сохранить заблокировано', + 'uk-ua': 'Зберегти заблоковано', + 'pt-br': 'Salvar bloqueado', }, saveBlockedDescription: { - "en-us": "Form cannot be saved because of the following error:", - "ru-ru": "Форму невозможно сохранить из-за следующей ошибки:", - "es-es": "No se puede guardar el formulario debido al siguiente error:", - "fr-fr": + 'en-us': 'Form cannot be saved because of the following error:', + 'ru-ru': 'Форму невозможно сохранить из-за следующей ошибки:', + 'es-es': 'No se puede guardar el formulario debido al siguiente error:', + 'fr-fr': "Le formulaire ne peut pas être enregistré en raison de l'erreur suivante :", - "uk-ua": "Форму неможливо зберегти через таку помилку:", - "de-ch": - "Das Formular kann aufgrund des folgenden Fehlers nicht gespeichert werden:", - "pt-br": "O formulário não pode ser salvo devido ao seguinte erro:", + 'uk-ua': 'Форму неможливо зберегти через таку помилку:', + 'de-ch': + 'Das Formular kann aufgrund des folgenden Fehlers nicht gespeichert werden:', + 'pt-br': 'O formulário não pode ser salvo devido ao seguinte erro:', }, unavailableCommandButton: { - "en-us": "Command N/A", - "ru-ru": "Команда N/A", - "es-es": "Comando N/A", - "fr-fr": "Commande N/A", - "uk-ua": "Команда Немає", - "de-ch": "Befehl N/A", - "pt-br": "Comando N/A", + 'en-us': 'Command N/A', + 'ru-ru': 'Команда N/A', + 'es-es': 'Comando N/A', + 'fr-fr': 'Commande N/A', + 'uk-ua': 'Команда Немає', + 'de-ch': 'Befehl N/A', + 'pt-br': 'Comando N/A', }, commandUnavailable: { - "en-us": "Command Not Available", - "ru-ru": "Команда недоступна", - "es-es": "Comando no disponible", - "fr-fr": "Commande non disponible", - "uk-ua": "Команда недоступна", - "de-ch": "Befehl nicht verfügbar", - "pt-br": "Comando não disponível", + 'en-us': 'Command Not Available', + 'ru-ru': 'Команда недоступна', + 'es-es': 'Comando no disponible', + 'fr-fr': 'Commande non disponible', + 'uk-ua': 'Команда недоступна', + 'de-ch': 'Befehl nicht verfügbar', + 'pt-br': 'Comando não disponível', }, commandUnavailableDescription: { - "en-us": "This command is currently unavailable for Specify 7.", - "ru-ru": "В настоящее время эта команда недоступна для Specify 7.", - "es-es": "Este comando no está disponible actualmente para Specify 7.", - "uk-ua": "Ця команда наразі недоступна для Specify 7.", - "de-ch": "Dieser Befehl ist derzeit für Specify 7 nicht verfügbar.", - "fr-fr": "Cette commande n'est actuellement pas disponible pour Specify 7.", - "pt-br": "Este comando não está disponível no momento para o Specify 7.", + 'en-us': 'This command is currently unavailable for Specify 7.', + 'ru-ru': 'В настоящее время эта команда недоступна для Specify 7.', + 'es-es': 'Este comando no está disponible actualmente para Specify 7.', + 'uk-ua': 'Ця команда наразі недоступна для Specify 7.', + 'de-ch': 'Dieser Befehl ist derzeit für Specify 7 nicht verfügbar.', + 'fr-fr': "Cette commande n'est actuellement pas disponible pour Specify 7.", + 'pt-br': 'Este comando não está disponível no momento para o Specify 7.', }, commandUnavailableSecondDescription: { - "en-us": - "It was probably included on this form from Specify 6 and may be supported in the future.", - "ru-ru": - "Вероятно, он был включен в эту форму из Specify 6 и может поддерживаться в будущем.", - "es-es": - "Probablemente se incluyó en este formulario de la Especificación 6 y es posible que se admita en el futuro.", - "fr-fr": + 'en-us': + 'It was probably included on this form from Specify 6 and may be supported in the future.', + 'ru-ru': + 'Вероятно, он был включен в эту форму из Specify 6 и может поддерживаться в будущем.', + 'es-es': + 'Probablemente se incluyó en este formulario de la Especificación 6 y es posible que se admita en el futuro.', + 'fr-fr': "Il a probablement été inclus dans ce formulaire à partir de Specify 6 et peut être pris en charge à l'avenir.", - "uk-ua": - "Ймовірно, це було включено до цієї форми з Specify 6 і може бути підтримано в майбутньому.", - "de-ch": - "Es war wahrscheinlich in diesem Formular von Specify 6 enthalten und wird möglicherweise in Zukunft unterstützt.", - "pt-br": - "Provavelmente foi incluído neste formulário do Specify 6 e pode ser suportado no futuro.", + 'uk-ua': + 'Ймовірно, це було включено до цієї форми з Specify 6 і може бути підтримано в майбутньому.', + 'de-ch': + 'Es war wahrscheinlich in diesem Formular von Specify 6 enthalten und wird möglicherweise in Zukunft unterstützt.', + 'pt-br': + 'Provavelmente foi incluído neste formulário do Specify 6 e pode ser suportado no futuro.', }, commandName: { - "en-us": "Command name", - "ru-ru": "Имя команды", - "es-es": "Nombre del comando", - "fr-fr": "Nom de la commande", - "uk-ua": "Назва команди", - "de-ch": "Befehlsname", - "pt-br": "Nome do comando", + 'en-us': 'Command name', + 'ru-ru': 'Имя команды', + 'es-es': 'Nombre del comando', + 'fr-fr': 'Nom de la commande', + 'uk-ua': 'Назва команди', + 'de-ch': 'Befehlsname', + 'pt-br': 'Nome do comando', }, unavailablePluginButton: { - "en-us": "Plugin N/A", - "ru-ru": "Плагин N/A", - "es-es": "Complemento N/A", - "fr-fr": "Plugin N/A", - "uk-ua": "Плагін Немає", - "de-ch": "Plugin N/A", - "pt-br": "Plugin N/A", + 'en-us': 'Plugin N/A', + 'ru-ru': 'Плагин N/A', + 'es-es': 'Complemento N/A', + 'fr-fr': 'Plugin N/A', + 'uk-ua': 'Плагін Немає', + 'de-ch': 'Plugin N/A', + 'pt-br': 'Plugin N/A', }, pluginNotAvailable: { - "en-us": "Plugin Not Available", - "ru-ru": "Плагин недоступен", - "es-es": "Complemento no disponible", - "fr-fr": "Plugin non disponible", - "uk-ua": "Плагін недоступний", - "de-ch": "Plugin nicht verfügbar", - "pt-br": "Plugin não disponível", + 'en-us': 'Plugin Not Available', + 'ru-ru': 'Плагин недоступен', + 'es-es': 'Complemento no disponible', + 'fr-fr': 'Plugin non disponible', + 'uk-ua': 'Плагін недоступний', + 'de-ch': 'Plugin nicht verfügbar', + 'pt-br': 'Plugin não disponível', }, pluginNotAvailableDescription: { - "en-us": "This plugin is currently unavailable for Specify 7", - "ru-ru": "Этот плагин в настоящее время недоступен для Specify 7", - "es-es": "Este complemento no está disponible actualmente para Specify 7", - "fr-fr": "Ce plugin n'est actuellement pas disponible pour Specify 7", - "uk-ua": "Цей плагін наразі недоступний для Specify 7", - "de-ch": "Dieses Plugin ist derzeit für Specify 7 nicht verfügbar", - "pt-br": "Este plugin não está disponível no momento para o Specify 7", + 'en-us': 'This plugin is currently unavailable for Specify 7', + 'ru-ru': 'Этот плагин в настоящее время недоступен для Specify 7', + 'es-es': 'Este complemento no está disponible actualmente para Specify 7', + 'fr-fr': "Ce plugin n'est actuellement pas disponible pour Specify 7", + 'uk-ua': 'Цей плагін наразі недоступний для Specify 7', + 'de-ch': 'Dieses Plugin ist derzeit für Specify 7 nicht verfügbar', + 'pt-br': 'Este plugin não está disponível no momento para o Specify 7', }, wrongTableForPlugin: { comment: - "Example: ... Locality, Collecting Event or Collection Object forms.", - "en-us": - "This plugin cannot be used on the {currentTable:string} form. Try moving it to the {supportedTables:string} forms.", - "ru-ru": - "Этот плагин нельзя использовать на форме {currentTable:string}. Попробуйте переместить его на формы {supportedTables:string}.", - "es-es": - "Este complemento no se puede utilizar en el formulario {currentTable:string}. Intente moverlo a los formularios {supportedTables:string}.", - "fr-fr": - "Ce plugin ne peut pas être utilisé sur le formulaire {currentTable:string}. Essayez de le déplacer vers les formulaires {supportedTables:string}.", - "uk-ua": - "Цей плагін не можна використовувати на формі {currentTable:string}. Спробуйте перемістити його на форми {supportedTables:string}.", - "de-ch": - "Dieses Plugin kann nicht im Formular {currentTable:string} verwendet werden. Versuchen Sie, es in die Formulare {supportedTables:string} zu verschieben.", - "pt-br": - "Este plugin não pode ser usado no formulário {currentTable:string}. Tente movê-lo para os formulários {supportedTables:string}.", + 'Example: ... Locality, Collecting Event or Collection Object forms.', + 'en-us': + 'This plugin cannot be used on the {currentTable:string} form. Try moving it to the {supportedTables:string} forms.', + 'ru-ru': + 'Этот плагин нельзя использовать на форме {currentTable:string}. Попробуйте переместить его на формы {supportedTables:string}.', + 'es-es': + 'Este complemento no se puede utilizar en el formulario {currentTable:string}. Intente moverlo a los formularios {supportedTables:string}.', + 'fr-fr': + 'Ce plugin ne peut pas être utilisé sur le formulaire {currentTable:string}. Essayez de le déplacer vers les formulaires {supportedTables:string}.', + 'uk-ua': + 'Цей плагін не можна використовувати на формі {currentTable:string}. Спробуйте перемістити його на форми {supportedTables:string}.', + 'de-ch': + 'Dieses Plugin kann nicht im Formular {currentTable:string} verwendet werden. Versuchen Sie, es in die Formulare {supportedTables:string} zu verschieben.', + 'pt-br': + 'Este plugin não pode ser usado no formulário {currentTable:string}. Tente movê-lo para os formulários {supportedTables:string}.', }, wrongTableForCommand: { - "en-us": - "The command cannot be used on the {currentTable:string} form. It can only be used on the {correctTable:string} form.", - "ru-ru": - "Команда не может быть использована в форме {currentTable:string}. Она может быть использована только в форме {correctTable:string}.", - "es-es": - "El comando no se puede utilizar en el formulario {currentTable:string}. Sólo se puede utilizar en el formulario {correctTable:string}.", - "fr-fr": - "La commande ne peut pas être utilisée sur le formulaire {currentTable:string}. Elle ne peut être utilisée que sur le formulaire {correctTable:string}.", - "uk-ua": - "Команду не можна використовувати у формі {currentTable:string}. Її можна використовувати лише у формі {correctTable:string}.", - "de-ch": - "Der Befehl kann nicht auf dem Formular {currentTable:string} verwendet werden. Er kann nur auf dem Formular {correctTable:string} verwendet werden.", - "pt-br": - "O comando não pode ser usado no formato {currentTable:string}. Ele só pode ser usado no formato {correctTable:string}.", + 'en-us': + 'The command cannot be used on the {currentTable:string} form. It can only be used on the {correctTable:string} form.', + 'ru-ru': + 'Команда не может быть использована в форме {currentTable:string}. Она может быть использована только в форме {correctTable:string}.', + 'es-es': + 'El comando no se puede utilizar en el formulario {currentTable:string}. Sólo se puede utilizar en el formulario {correctTable:string}.', + 'fr-fr': + 'La commande ne peut pas être utilisée sur le formulaire {currentTable:string}. Elle ne peut être utilisée que sur le formulaire {correctTable:string}.', + 'uk-ua': + 'Команду не можна використовувати у формі {currentTable:string}. Її можна використовувати лише у формі {correctTable:string}.', + 'de-ch': + 'Der Befehl kann nicht auf dem Formular {currentTable:string} verwendet werden. Er kann nur auf dem Formular {correctTable:string} verwendet werden.', + 'pt-br': + 'O comando não pode ser usado no formato {currentTable:string}. Ele só pode ser usado no formato {correctTable:string}.', }, pluginName: { - "en-us": "Plugin name", - "ru-ru": "Имя плагина", - "es-es": "Nombre del complemento", - "fr-fr": "Nom du plugin", - "uk-ua": "Назва плагіна", - "de-ch": "Plugin-Name", - "pt-br": "Nome do plugin", + 'en-us': 'Plugin name', + 'ru-ru': 'Имя плагина', + 'es-es': 'Nombre del complemento', + 'fr-fr': 'Nom du plugin', + 'uk-ua': 'Назва плагіна', + 'de-ch': 'Plugin-Name', + 'pt-br': 'Nome do plugin', }, illegalBool: { comment: ` Yes/No probably shouldn't be translated as Specify 7 does not support changing which values are recognized as Yes/No in a given language `, - "en-us": "Illegal value for a Yes/No field", - "ru-ru": "Недопустимое значение для поля Да/Нет", - "es-es": "Valor ilegal para un campo Sí/No", - "fr-fr": "Valeur illégale pour un champ Oui/Non", - "uk-ua": "Неприпустиме значення для поля «Так/Ні»", - "de-ch": "Unzulässiger Wert für ein Ja/Nein-Feld", - "pt-br": "Valor ilegal para um campo Sim/Não", + 'en-us': 'Illegal value for a Yes/No field', + 'ru-ru': 'Недопустимое значение для поля Да/Нет', + 'es-es': 'Valor ilegal para un campo Sí/No', + 'fr-fr': 'Valeur illégale pour un champ Oui/Non', + 'uk-ua': 'Неприпустиме значення для поля «Так/Ні»', + 'de-ch': 'Unzulässiger Wert für ein Ja/Nein-Feld', + 'pt-br': 'Valor ilegal para um campo Sim/Não', }, requiredField: { - "en-us": "Field is required.", - "ru-ru": "Поле обязательно для заполнения.", - "es-es": "Se requiere campo.", - "fr-fr": "Le champ est obligatoire.", - "uk-ua": "Поле обов'язкове для заповнення.", - "de-ch": "Pflichtfeld.", - "pt-br": "Campo obrigatório.", + 'en-us': 'Field is required.', + 'ru-ru': 'Поле обязательно для заполнения.', + 'es-es': 'Se requiere campo.', + 'fr-fr': 'Le champ est obligatoire.', + 'uk-ua': "Поле обов'язкове для заповнення.", + 'de-ch': 'Pflichtfeld.', + 'pt-br': 'Campo obrigatório.', }, invalidValue: { - "en-us": "Invalid value", - "ru-ru": "Неверное значение", - "es-es": "Hoy", - "fr-fr": "Valeur invalide", - "uk-ua": "Недійсне значення", - "de-ch": "Ungültiger Wert", - "pt-br": "Valor inválido", + 'en-us': 'Invalid value', + 'ru-ru': 'Неверное значение', + 'es-es': 'Hoy', + 'fr-fr': 'Valeur invalide', + 'uk-ua': 'Недійсне значення', + 'de-ch': 'Ungültiger Wert', + 'pt-br': 'Valor inválido', }, requiredFormat: { - comment: "Used in field validation messages on the form", - "en-us": "Required Format: {format:string}.", - "ru-ru": "Требуемый формат: {format:string}.", - "es-es": "Formato requerido: {format:string}.", - "fr-fr": "Format requis : {format:string}.", - "uk-ua": "Необхідний формат: {format:string}.", - "de-ch": "Erforderliches Format: {format:string}.", - "pt-br": "Formato necessário: {format:string}.", + comment: 'Used in field validation messages on the form', + 'en-us': 'Required Format: {format:string}.', + 'ru-ru': 'Требуемый формат: {format:string}.', + 'es-es': 'Formato requerido: {format:string}.', + 'fr-fr': 'Format requis : {format:string}.', + 'uk-ua': 'Необхідний формат: {format:string}.', + 'de-ch': 'Erforderliches Format: {format:string}.', + 'pt-br': 'Formato necessário: {format:string}.', }, inputTypeNumber: { - "en-us": "Value must be a number", - "ru-ru": "Значение должно быть числом.", - "es-es": "El valor debe ser un número.", - "uk-ua": "Значення має бути числом", - "de-ch": "Der Wert muss eine Zahl sein", - "fr-fr": "La valeur doit être un nombre", - "pt-br": "O valor deve ser um número", + 'en-us': 'Value must be a number', + 'ru-ru': 'Значение должно быть числом.', + 'es-es': 'El valor debe ser un número.', + 'uk-ua': 'Значення має бути числом', + 'de-ch': 'Der Wert muss eine Zahl sein', + 'fr-fr': 'La valeur doit être un nombre', + 'pt-br': 'O valor deve ser um número', }, organization: { - "en-us": "Organization", - "ru-ru": "Организация", - "es-es": "Organización", - "fr-fr": "Organisation", - "uk-ua": "Організація", - "de-ch": "Organisation", - "pt-br": "Organização", + 'en-us': 'Organization', + 'ru-ru': 'Организация', + 'es-es': 'Organización', + 'fr-fr': 'Organisation', + 'uk-ua': 'Організація', + 'de-ch': 'Organisation', + 'pt-br': 'Organização', }, person: { - "en-us": "Person", - "ru-ru": "Человек", - "es-es": "Persona", - "fr-fr": "Personne", - "uk-ua": "Людина", - "de-ch": "Person", - "pt-br": "Pessoa", + 'en-us': 'Person', + 'ru-ru': 'Человек', + 'es-es': 'Persona', + 'fr-fr': 'Personne', + 'uk-ua': 'Людина', + 'de-ch': 'Person', + 'pt-br': 'Pessoa', }, other: { - "en-us": "Other", - "ru-ru": "Другой", - "es-es": "Otro", - "fr-fr": "Autre", - "uk-ua": "Інше", - "de-ch": "Andere", - "pt-br": "Outro", + 'en-us': 'Other', + 'ru-ru': 'Другой', + 'es-es': 'Otro', + 'fr-fr': 'Autre', + 'uk-ua': 'Інше', + 'de-ch': 'Andere', + 'pt-br': 'Outro', }, group: { - "en-us": "Group", - "ru-ru": "Группа", - "es-es": "Grupo", - "fr-fr": "Groupe", - "uk-ua": "Група", - "de-ch": "Gruppe", - "pt-br": "Grupo", + 'en-us': 'Group', + 'ru-ru': 'Группа', + 'es-es': 'Grupo', + 'fr-fr': 'Groupe', + 'uk-ua': 'Група', + 'de-ch': 'Gruppe', + 'pt-br': 'Grupo', }, userDefinedItems: { - "en-us": "User Defined Items", - "ru-ru": "Элементы, определяемые пользователем", - "es-es": "Elementos definidos por el usuario", - "fr-fr": "Éléments définis par l'utilisateur", - "uk-ua": "Елементи, визначені користувачем", - "de-ch": "Benutzerdefinierte Elemente", - "pt-br": "Itens definidos pelo usuário", + 'en-us': 'User Defined Items', + 'ru-ru': 'Элементы, определяемые пользователем', + 'es-es': 'Elementos definidos por el usuario', + 'fr-fr': "Éléments définis par l'utilisateur", + 'uk-ua': 'Елементи, визначені користувачем', + 'de-ch': 'Benutzerdefinierte Elemente', + 'pt-br': 'Itens definidos pelo usuário', }, entireTable: { - "en-us": "Entire Table", - "ru-ru": "Вся таблица", - "es-es": "Tabla entera", - "fr-fr": "Tableau entier", - "uk-ua": "Вся таблиця", - "de-ch": "Gesamte Tabelle", - "pt-br": "Mesa inteira", + 'en-us': 'Entire Table', + 'ru-ru': 'Вся таблица', + 'es-es': 'Tabla entera', + 'fr-fr': 'Tableau entier', + 'uk-ua': 'Вся таблиця', + 'de-ch': 'Gesamte Tabelle', + 'pt-br': 'Mesa inteira', }, fieldFromTable: { - "en-us": "Field From Table", - "ru-ru": "Поле из таблицы", - "es-es": "Campo de la tabla", - "fr-fr": "Champ de la table", - "uk-ua": "Поле з таблиці", - "de-ch": "Feld aus Tabelle", - "pt-br": "Campo da Tabela", + 'en-us': 'Field From Table', + 'ru-ru': 'Поле из таблицы', + 'es-es': 'Campo de la tabla', + 'fr-fr': 'Champ de la table', + 'uk-ua': 'Поле з таблиці', + 'de-ch': 'Feld aus Tabelle', + 'pt-br': 'Campo da Tabela', }, unsupportedCellType: { - "en-us": "Unsupported cell type", - "ru-ru": "Неподдерживаемый тип ячейки", - "es-es": "Tipo de celda no compatible", - "fr-fr": "Type de cellule non pris en charge", - "uk-ua": "Непідтримуваний тип клітинки", - "de-ch": "Nicht unterstützter Zelltyp", - "pt-br": "Tipo de célula não suportado", + 'en-us': 'Unsupported cell type', + 'ru-ru': 'Неподдерживаемый тип ячейки', + 'es-es': 'Tipo de celda no compatible', + 'fr-fr': 'Type de cellule non pris en charge', + 'uk-ua': 'Непідтримуваний тип клітинки', + 'de-ch': 'Nicht unterstützter Zelltyp', + 'pt-br': 'Tipo de célula não suportado', }, additionalResultsOmitted: { comment: ` Represents truncated search dialog output (when lots of results returned) `, - "en-us": "Additional results omitted", - "ru-ru": "Дополнительные результаты пропущены", - "es-es": "Resultados adicionales omitidos", - "fr-fr": "Résultats supplémentaires omis", - "uk-ua": "Додаткові результати пропущені", - "de-ch": "Weitere Ergebnisse ausgelassen", - "pt-br": "Resultados adicionais omitidos", + 'en-us': 'Additional results omitted', + 'ru-ru': 'Дополнительные результаты пропущены', + 'es-es': 'Resultados adicionales omitidos', + 'fr-fr': 'Résultats supplémentaires omis', + 'uk-ua': 'Додаткові результати пропущені', + 'de-ch': 'Weitere Ergebnisse ausgelassen', + 'pt-br': 'Resultados adicionais omitidos', }, recordSelectorUnloadProtect: { - "en-us": "Proceed without saving?", - "ru-ru": "Продолжить без сохранения?", - "es-es": "¿Continuar sin guardar?", - "fr-fr": "Continuer sans enregistrer ?", - "uk-ua": "Продовжити без збереження?", - "de-ch": "Ohne Speichern fortfahren?", - "pt-br": "Continuar sem salvar?", + 'en-us': 'Proceed without saving?', + 'ru-ru': 'Продолжить без сохранения?', + 'es-es': '¿Continuar sin guardar?', + 'fr-fr': 'Continuer sans enregistrer ?', + 'uk-ua': 'Продовжити без збереження?', + 'de-ch': 'Ohne Speichern fortfahren?', + 'pt-br': 'Continuar sem salvar?', }, recordSelectorUnloadProtectDescription: { comment: ` When in record set and current record is unsaved and try to navigate to another record `, - "en-us": "You might want to save this record before navigating away.", - "ru-ru": "Возможно, вам захочется сохранить эту запись, прежде чем уйти.", - "es-es": "Es posible que desees guardar este registro antes de navegar.", - "fr-fr": - "Vous souhaiterez peut-être sauvegarder cet enregistrement avant de partir.", - "uk-ua": - "Можливо, ви захочете зберегти цей запис, перш ніж залишати сторінку.", - "de-ch": - "Möglicherweise möchten Sie diesen Datensatz speichern, bevor Sie wegnavigieren.", - "pt-br": "Talvez você queira salvar este registro antes de sair navegando.", + 'en-us': 'You might want to save this record before navigating away.', + 'ru-ru': 'Возможно, вам захочется сохранить эту запись, прежде чем уйти.', + 'es-es': 'Es posible que desees guardar este registro antes de navegar.', + 'fr-fr': + 'Vous souhaiterez peut-être sauvegarder cet enregistrement avant de partir.', + 'uk-ua': + 'Можливо, ви захочете зберегти цей запис, перш ніж залишати сторінку.', + 'de-ch': + 'Möglicherweise möchten Sie diesen Datensatz speichern, bevor Sie wegnavigieren.', + 'pt-br': 'Talvez você queira salvar este registro antes de sair navegando.', }, creatingNewRecord: { - "en-us": "Creating new record", - "ru-ru": "Создание новой записи", - "es-es": "Creando nuevo registro", - "fr-fr": "Création d'un nouvel enregistrement", - "uk-ua": "Створення нового запису", - "de-ch": "Neuen Datensatz erstellen", - "pt-br": "Criando novo registro", + 'en-us': 'Creating new record', + 'ru-ru': 'Создание новой записи', + 'es-es': 'Creando nuevo registro', + 'fr-fr': "Création d'un nouvel enregistrement", + 'uk-ua': 'Створення нового запису', + 'de-ch': 'Neuen Datensatz erstellen', + 'pt-br': 'Criando novo registro', }, createNewRecordSet: { - "en-us": "Create a new record set", - "ru-ru": "Создать новый набор записей", - "es-es": "Crear un nuevo conjunto de registros", - "fr-fr": "Créer un nouvel ensemble d'enregistrements", - "uk-ua": "Створити новий набір записів", - "de-ch": "Erstellen Sie einen neuen Datensatz", - "pt-br": "Criar um novo conjunto de registros", + 'en-us': 'Create a new record set', + 'ru-ru': 'Создать новый набор записей', + 'es-es': 'Crear un nuevo conjunto de registros', + 'fr-fr': "Créer un nouvel ensemble d'enregistrements", + 'uk-ua': 'Створити новий набір записів', + 'de-ch': 'Erstellen Sie einen neuen Datensatz', + 'pt-br': 'Criar um novo conjunto de registros', }, forward: { - "en-us": "Forward", - "ru-ru": "Вперед", - "es-es": "Adelante", - "fr-fr": "Avant", - "uk-ua": "Вперед", - "de-ch": "Nach vorne", - "pt-br": "Avançar", + 'en-us': 'Forward', + 'ru-ru': 'Вперед', + 'es-es': 'Adelante', + 'fr-fr': 'Avant', + 'uk-ua': 'Вперед', + 'de-ch': 'Nach vorne', + 'pt-br': 'Avançar', }, reverse: { - "en-us": "Reverse", - "ru-ru": "Обеспечить регресс", - "es-es": "Contrarrestar", - "fr-fr": "Inverse", - "uk-ua": "Зворотний", - "de-ch": "Umkehren", - "pt-br": "Reverter", + 'en-us': 'Reverse', + 'ru-ru': 'Обеспечить регресс', + 'es-es': 'Contrarrestar', + 'fr-fr': 'Inverse', + 'uk-ua': 'Зворотний', + 'de-ch': 'Umkehren', + 'pt-br': 'Reverter', }, deletedInline: { - "en-us": "(deleted)", - "ru-ru": "(удалено)", - "es-es": "(eliminado)", - "fr-fr": "(supprimé)", - "uk-ua": "(видалено)", - "de-ch": "(gestrichen)", - "pt-br": "(apagado)", + 'en-us': '(deleted)', + 'ru-ru': '(удалено)', + 'es-es': '(eliminado)', + 'fr-fr': '(supprimé)', + 'uk-ua': '(видалено)', + 'de-ch': '(gestrichen)', + 'pt-br': '(apagado)', }, duplicateRecordSetItem: { - comment: "Example: Duplicate Record Set Item", - "en-us": "Duplicate {recordSetItemTable:string}", - "ru-ru": "Дубликат {recordSetItemTable:string}", - "es-es": "Duplicado {recordSetItemTable:string}", - "uk-ua": "Дублікат {recordSetItemTable:string}", - "de-ch": "Duplikat {recordSetItemTable:string}", - "fr-fr": "Dupliquer {recordSetItemTable:string}", - "pt-br": "Duplicado {recordSetItemTable:string}", + comment: 'Example: Duplicate Record Set Item', + 'en-us': 'Duplicate {recordSetItemTable:string}', + 'ru-ru': 'Дубликат {recordSetItemTable:string}', + 'es-es': 'Duplicado {recordSetItemTable:string}', + 'uk-ua': 'Дублікат {recordSetItemTable:string}', + 'de-ch': 'Duplikat {recordSetItemTable:string}', + 'fr-fr': 'Dupliquer {recordSetItemTable:string}', + 'pt-br': 'Duplicado {recordSetItemTable:string}', }, duplicateRecordSetItemDescription: { - "en-us": - "This record is already present in the current {recordSetTable:string}", - "ru-ru": "Эта запись уже присутствует в текущем {recordSetTable:string}", - "es-es": - "Este registro ya está presente en el actual {recordSetTable:string}", - "fr-fr": - "Cet enregistrement est déjà présent dans le {recordSetTable:string} actuel", - "uk-ua": "Цей запис вже присутній у поточному {recordSetTable:string}", - "de-ch": - "Dieser Datensatz ist bereits im aktuellen {recordSetTable:string} vorhanden.", - "pt-br": "Este registro já está presente no atual {recordSetTable:string}", + 'en-us': + 'This record is already present in the current {recordSetTable:string}', + 'ru-ru': 'Эта запись уже присутствует в текущем {recordSetTable:string}', + 'es-es': + 'Este registro ya está presente en el actual {recordSetTable:string}', + 'fr-fr': + 'Cet enregistrement est déjà présent dans le {recordSetTable:string} actuel', + 'uk-ua': 'Цей запис вже присутній у поточному {recordSetTable:string}', + 'de-ch': + 'Dieser Datensatz ist bereits im aktuellen {recordSetTable:string} vorhanden.', + 'pt-br': 'Este registro já está presente no atual {recordSetTable:string}', }, addToRecordSet: { - "en-us": "Add to {recordSetTable:string}", - "ru-ru": "Добавить в {recordSetTable:string}", - "es-es": "Añadir a {recordSetTable:string}", - "fr-fr": "Ajouter à {recordSetTable:string}", - "uk-ua": "Додати до {recordSetTable:string}", - "de-ch": "Hinzufügen zu {recordSetTable:string}", - "pt-br": "Adicionar a {recordSetTable:string}", + 'en-us': 'Add to {recordSetTable:string}', + 'ru-ru': 'Добавить в {recordSetTable:string}', + 'es-es': 'Añadir a {recordSetTable:string}', + 'fr-fr': 'Ajouter à {recordSetTable:string}', + 'uk-ua': 'Додати до {recordSetTable:string}', + 'de-ch': 'Hinzufügen zu {recordSetTable:string}', + 'pt-br': 'Adicionar a {recordSetTable:string}', }, removeFromRecordSet: { - "en-us": "Remove from {recordSetTable:string}", - "ru-ru": "Удалить из {recordSetTable:string}", - "es-es": "Eliminar de {recordSetTable:string}", - "fr-fr": "Supprimer de {recordSetTable:string}", - "uk-ua": "Видалити з {recordSetTable:string}", - "de-ch": "Entfernen aus {recordSetTable:string}", - "pt-br": "Remover de {recordSetTable:string}", + 'en-us': 'Remove from {recordSetTable:string}', + 'ru-ru': 'Удалить из {recordSetTable:string}', + 'es-es': 'Eliminar de {recordSetTable:string}', + 'fr-fr': 'Supprimer de {recordSetTable:string}', + 'uk-ua': 'Видалити з {recordSetTable:string}', + 'de-ch': 'Entfernen aus {recordSetTable:string}', + 'pt-br': 'Remover de {recordSetTable:string}', }, nothingFound: { - "en-us": "Nothing found", - "ru-ru": "Ничего не найдено", - "es-es": "No se encontró nada", - "fr-fr": "Rien trouvé", - "uk-ua": "Нічого не знайдено", - "de-ch": "Nichts gefunden", - "pt-br": "Nada encontrado", + 'en-us': 'Nothing found', + 'ru-ru': 'Ничего не найдено', + 'es-es': 'No se encontró nada', + 'fr-fr': 'Rien trouvé', + 'uk-ua': 'Нічого не знайдено', + 'de-ch': 'Nichts gefunden', + 'pt-br': 'Nada encontrado', }, carryForward: { - comment: "Verb. Button label", - "en-us": "Carry Forward", - "ru-ru": "Перенести вперед", - "es-es": "Llevar adelante", - "fr-fr": "Reporter", - "uk-ua": "Перенести далі", - "de-ch": "Übertrag", - "pt-br": "Levar adiante", + comment: 'Verb. Button label', + 'en-us': 'Carry Forward', + 'ru-ru': 'Перенести вперед', + 'es-es': 'Llevar adelante', + 'fr-fr': 'Reporter', + 'uk-ua': 'Перенести далі', + 'de-ch': 'Übertrag', + 'pt-br': 'Levar adiante', }, carryForwardEnabled: { - "en-us": "Show Carry Forward button", - "ru-ru": "Показать кнопку «Перенести вперед»", - "es-es": "Mostrar el botón Llevar adelante", - "fr-fr": "Afficher le bouton de report", - "uk-ua": "Показати кнопку «Перенести вперед»", - "de-ch": "Schaltfläche „Übertrag anzeigen“", - "pt-br": "Mostrar botão Transferir para frente", + 'en-us': 'Show Carry Forward button', + 'ru-ru': 'Показать кнопку «Перенести вперед»', + 'es-es': 'Mostrar el botón Llevar adelante', + 'fr-fr': 'Afficher le bouton de report', + 'uk-ua': 'Показати кнопку «Перенести вперед»', + 'de-ch': 'Schaltfläche „Übertrag anzeigen“', + 'pt-br': 'Mostrar botão Transferir para frente', }, bulkCarryForwardEnabled: { - "en-us": "Show Bulk Carry Forward count", - "de-ch": "Anzahl der Massenüberträge anzeigen", - "es-es": "Mostrar recuento de transferencia masiva", - "fr-fr": "Afficher le nombre de reports en masse", - "pt-br": "Mostrar contagem de transporte em massa", - "ru-ru": "Показать количество переносов на следующую страницу", - "uk-ua": "Показати кількість групового перенесення", + 'en-us': 'Show Bulk Carry Forward count', + 'de-ch': 'Anzahl der Massenüberträge anzeigen', + 'es-es': 'Mostrar recuento de transferencia masiva', + 'fr-fr': 'Afficher le nombre de reports en masse', + 'pt-br': 'Mostrar contagem de transporte em massa', + 'ru-ru': 'Показать количество переносов на следующую страницу', + 'uk-ua': 'Показати кількість групового перенесення', }, bulkCarryForwardCount: { - "en-us": "Bulk Carry Forward count", - "de-ch": "Anzahl der Massenüberträge", - "es-es": "Recuento de transferencia masiva", - "fr-fr": "Nombre de reports en masse", - "pt-br": "Contagem de transporte de carga a granel", - "ru-ru": "Массовый перенос данных на следующую страницу", - "uk-ua": "Кількість перенесених даних", + 'en-us': 'Bulk Carry Forward count', + 'de-ch': 'Anzahl der Massenüberträge', + 'es-es': 'Recuento de transferencia masiva', + 'fr-fr': 'Nombre de reports en masse', + 'pt-br': 'Contagem de transporte de carga a granel', + 'ru-ru': 'Массовый перенос данных на следующую страницу', + 'uk-ua': 'Кількість перенесених даних', }, carryForwardDescription: { - "en-us": "Create a new record with certain fields carried over", - "ru-ru": "Создать новую запись с перенесенными определенными полями", - "es-es": "Crear un nuevo registro con ciertos campos transferidos", - "fr-fr": "Créer un nouvel enregistrement avec certains champs reportés", - "uk-ua": "Створити новий запис із перенесенням певних полів", - "de-ch": - "Erstellen Sie einen neuen Datensatz mit bestimmten übernommenen Feldern", - "pt-br": "Crie um novo registro com determinados campos transferidos", + 'en-us': 'Create a new record with certain fields carried over', + 'ru-ru': 'Создать новую запись с перенесенными определенными полями', + 'es-es': 'Crear un nuevo registro con ciertos campos transferidos', + 'fr-fr': 'Créer un nouvel enregistrement avec certains champs reportés', + 'uk-ua': 'Створити новий запис із перенесенням певних полів', + 'de-ch': + 'Erstellen Sie einen neuen Datensatz mit bestimmten übernommenen Feldern', + 'pt-br': 'Crie um novo registro com determinados campos transferidos', }, carryForwardSettingsDescription: { - "en-us": "Configure fields to carry forward", - "ru-ru": "Настройте поля для переноса", - "es-es": "Configurar campos para transferir", - "fr-fr": "Configurer les champs à reporter", - "uk-ua": "Налаштуйте поля для перенесення", - "de-ch": "Konfigurieren der zu übertragenden Felder", - "pt-br": "Configurar campos para levar adiante", + 'en-us': 'Configure fields to carry forward', + 'ru-ru': 'Настройте поля для переноса', + 'es-es': 'Configurar campos para transferir', + 'fr-fr': 'Configurer les champs à reporter', + 'uk-ua': 'Налаштуйте поля для перенесення', + 'de-ch': 'Konfigurieren der zu übertragenden Felder', + 'pt-br': 'Configurar campos para levar adiante', }, bulkCarryForwardSettingsDescription: { - "en-us": "Configure fields to bulk carry forward", - "de-ch": "Konfigurieren von Feldern für die Massenübertragung", - "es-es": "Configurar campos para transferirlos en masa", - "fr-fr": "Configurer les champs pour un report en masse", - "pt-br": "Configurar campos para transporte em massa", - "ru-ru": "Настройте поля для массового переноса данных", - "uk-ua": "Налаштуйте поля для масового перенесення", + 'en-us': 'Configure fields to bulk carry forward', + 'de-ch': 'Konfigurieren von Feldern für die Massenübertragung', + 'es-es': 'Configurar campos para transferirlos en masa', + 'fr-fr': 'Configurer les champs pour un report en masse', + 'pt-br': 'Configurar campos para transporte em massa', + 'ru-ru': 'Настройте поля для массового переноса данных', + 'uk-ua': 'Налаштуйте поля для масового перенесення', }, carryForwardTableSettingsDescription: { - "en-us": "Configure fields to carry forward ({tableName:string})", - "ru-ru": "Настройте поля для переноса ({tableName:string})", - "es-es": "Configurar campos para transferir ({tableName:string})", - "fr-fr": "Configurer les champs à reporter ({tableName:string})", - "uk-ua": "Налаштуйте поля для перенесення ({tableName:string})", - "de-ch": - "Konfigurieren Sie die zu übertragenden Felder ({tableName:string})", - "pt-br": "Configurar campos para levar adiante ({tableName:string})", + 'en-us': 'Configure fields to carry forward ({tableName:string})', + 'ru-ru': 'Настройте поля для переноса ({tableName:string})', + 'es-es': 'Configurar campos para transferir ({tableName:string})', + 'fr-fr': 'Configurer les champs à reporter ({tableName:string})', + 'uk-ua': 'Налаштуйте поля для перенесення ({tableName:string})', + 'de-ch': + 'Konfigurieren Sie die zu übertragenden Felder ({tableName:string})', + 'pt-br': 'Configurar campos para levar adiante ({tableName:string})', }, bulkCarryForwardTableSettingsDescription: { - "en-us": "Configure fields to bulk carry forward ({tableName:string})", - "de-ch": - "Konfigurieren Sie Felder für den Massenübertrag ({tableName:string})", - "es-es": - "Configurar campos para transferirlos en masa ({tableName:string})", - "fr-fr": - "Configurer les champs pour un report en masse ({tableName:string})", - "pt-br": "Configurar campos para transporte em massa ({tableName:string})", - "ru-ru": "Настройте поля для массового переноса ({tableName:string})", - "uk-ua": "Налаштуйте поля для масового перенесення ({tableName:string})", + 'en-us': 'Configure fields to bulk carry forward ({tableName:string})', + 'de-ch': + 'Konfigurieren Sie Felder für den Massenübertrag ({tableName:string})', + 'es-es': + 'Configurar campos para transferirlos en masa ({tableName:string})', + 'fr-fr': + 'Configurer les champs pour un report en masse ({tableName:string})', + 'pt-br': 'Configurar campos para transporte em massa ({tableName:string})', + 'ru-ru': 'Настройте поля для массового переноса ({tableName:string})', + 'uk-ua': 'Налаштуйте поля для масового перенесення ({tableName:string})', }, carryForwardUniqueField: { - "en-us": "This field must be unique. It can not be carried over", - "ru-ru": "Это поле должно быть уникальным. Оно не может быть перенесено.", - "es-es": "Este campo debe ser único. No se puede transferir.", - "fr-fr": "Ce champ doit être unique. Il ne peut pas être reporté.", - "uk-ua": "Це поле має бути унікальним. Його не можна переносити", - "de-ch": "Dieses Feld muss eindeutig sein. Es kann nicht übertragen werden", - "pt-br": "Este campo deve ser único. Não pode ser transferido", + 'en-us': 'This field must be unique. It can not be carried over', + 'ru-ru': 'Это поле должно быть уникальным. Оно не может быть перенесено.', + 'es-es': 'Este campo debe ser único. No se puede transferir.', + 'fr-fr': 'Ce champ doit être unique. Il ne peut pas être reporté.', + 'uk-ua': 'Це поле має бути унікальним. Його не можна переносити', + 'de-ch': 'Dieses Feld muss eindeutig sein. Es kann nicht übertragen werden', + 'pt-br': 'Este campo deve ser único. Não pode ser transferido', }, carryForwardRequiredField: { - "en-us": "This field is required. It must be carried forward", - "ru-ru": "Это поле обязательно. Его необходимо перенести", - "es-es": "Este campo es obligatorio. Debe ser transferido", - "fr-fr": "Ce champ est obligatoire. Il doit être reporté", - "uk-ua": "Це поле обов'язкове. Його потрібно перенести", - "de-ch": "Dieses Feld ist erforderlich. Es muss übertragen werden", - "pt-br": "Este campo é obrigatório. Deve ser levado adiante", + 'en-us': 'This field is required. It must be carried forward', + 'ru-ru': 'Это поле обязательно. Его необходимо перенести', + 'es-es': 'Este campo es obligatorio. Debe ser transferido', + 'fr-fr': 'Ce champ est obligatoire. Il doit être reporté', + 'uk-ua': "Це поле обов'язкове. Його потрібно перенести", + 'de-ch': 'Dieses Feld ist erforderlich. Es muss übertragen werden', + 'pt-br': 'Este campo é obrigatório. Deve ser levado adiante', }, cloneButtonEnabled: { - "en-us": "Show Clone button", - "ru-ru": "Показать кнопку «Клонировать»", - "es-es": "Mostrar botón Clonar", - "fr-fr": "Afficher le bouton Cloner", - "uk-ua": "Кнопка «Показати клон»", - "de-ch": "Schaltfläche „Klonen“ anzeigen", - "pt-br": "Mostrar botão Clonar", + 'en-us': 'Show Clone button', + 'ru-ru': 'Показать кнопку «Клонировать»', + 'es-es': 'Mostrar botón Clonar', + 'fr-fr': 'Afficher le bouton Cloner', + 'uk-ua': 'Кнопка «Показати клон»', + 'de-ch': 'Schaltfläche „Klonen“ anzeigen', + 'pt-br': 'Mostrar botão Clonar', }, addButtonEnabled: { - "en-us": "Show Add button", - "ru-ru": "Показать кнопку «Добавить»", - "es-es": "Mostrar el botón Agregar", - "fr-fr": "Afficher le bouton Ajouter", - "uk-ua": "Показати кнопку «Додати»", - "de-ch": "Schaltfläche „Hinzufügen“ anzeigen", - "pt-br": "Mostrar botão Adicionar", + 'en-us': 'Show Add button', + 'ru-ru': 'Показать кнопку «Добавить»', + 'es-es': 'Mostrar el botón Agregar', + 'fr-fr': 'Afficher le bouton Ajouter', + 'uk-ua': 'Показати кнопку «Додати»', + 'de-ch': 'Schaltfläche „Hinzufügen“ anzeigen', + 'pt-br': 'Mostrar botão Adicionar', }, addButtonDescription: { - "en-us": "Create a new blank record", - "ru-ru": "Создать новую пустую запись", - "es-es": "Crear un nuevo registro en blanco", - "fr-fr": "Créer un nouvel enregistrement vierge", - "uk-ua": "Створити новий пустий запис", - "de-ch": "Erstellen Sie einen neuen leeren Datensatz", - "pt-br": "Criar um novo registro em branco", + 'en-us': 'Create a new blank record', + 'ru-ru': 'Создать новую пустую запись', + 'es-es': 'Crear un nuevo registro en blanco', + 'fr-fr': 'Créer un nouvel enregistrement vierge', + 'uk-ua': 'Створити новий пустий запис', + 'de-ch': 'Erstellen Sie einen neuen leeren Datensatz', + 'pt-br': 'Criar um novo registro em branco', }, autoNumbering: { - "en-us": "Auto Numbering", - "ru-ru": "Автоматическая нумерация", - "es-es": "Numeración automática", - "fr-fr": "Numérotation automatique", - "uk-ua": "Автоматична нумерація", - "de-ch": "Automatische Nummerierung", - "pt-br": "Numeração automática", + 'en-us': 'Auto Numbering', + 'ru-ru': 'Автоматическая нумерация', + 'es-es': 'Numeración automática', + 'fr-fr': 'Numérotation automatique', + 'uk-ua': 'Автоматична нумерація', + 'de-ch': 'Automatische Nummerierung', + 'pt-br': 'Numeração automática', }, editFormDefinition: { - "en-us": "Edit Form Definition", - "ru-ru": "Редактировать определение формы", - "es-es": "Editar definición de formulario", - "fr-fr": "Modifier la définition du formulaire", - "uk-ua": "Редагувати визначення форми", - "de-ch": "Formulardefinition bearbeiten", - "pt-br": "Editar definição de formulário", + 'en-us': 'Edit Form Definition', + 'ru-ru': 'Редактировать определение формы', + 'es-es': 'Editar definición de formulario', + 'fr-fr': 'Modifier la définition du formulaire', + 'uk-ua': 'Редагувати визначення форми', + 'de-ch': 'Formulardefinition bearbeiten', + 'pt-br': 'Editar definição de formulário', }, useAutoGeneratedForm: { - "en-us": "Use Auto Generated Form", - "ru-ru": "Использовать автоматически сгенерированную форму", - "es-es": "Utilice el formulario generado automáticamente", - "fr-fr": "Utiliser le formulaire généré automatiquement", - "uk-ua": "Використати автоматично згенеровану форму", - "de-ch": "Automatisch generiertes Formular verwenden", - "pt-br": "Usar formulário gerado automaticamente", + 'en-us': 'Use Auto Generated Form', + 'ru-ru': 'Использовать автоматически сгенерированную форму', + 'es-es': 'Utilice el formulario generado automáticamente', + 'fr-fr': 'Utiliser le formulaire généré automatiquement', + 'uk-ua': 'Використати автоматично згенеровану форму', + 'de-ch': 'Automatisch generiertes Formular verwenden', + 'pt-br': 'Usar formulário gerado automaticamente', }, useFieldLabels: { - "en-us": "Use Localized Field Labels", - "ru-ru": "Используйте локализованные метки полей", - "es-es": "Utilice etiquetas de campo localizadas", - "fr-fr": "Utiliser des étiquettes de champ localisées", - "uk-ua": "Використовуйте локалізовані мітки полів", - "de-ch": "Lokalisierte Feldbezeichnungen verwenden", - "pt-br": "Use rótulos de campo localizados", + 'en-us': 'Use Localized Field Labels', + 'ru-ru': 'Используйте локализованные метки полей', + 'es-es': 'Utilice etiquetas de campo localizadas', + 'fr-fr': 'Utiliser des étiquettes de champ localisées', + 'uk-ua': 'Використовуйте локалізовані мітки полів', + 'de-ch': 'Lokalisierte Feldbezeichnungen verwenden', + 'pt-br': 'Use rótulos de campo localizados', }, showFieldLabels: { - "en-us": "Show Localized Field Labels", - "de-ch": "Lokalisierte Feldbezeichnungen anzeigen", - "es-es": "Mostrar etiquetas de campos localizados", - "fr-fr": "Afficher les étiquettes de champ localisées", - "ru-ru": "Показать локализованные метки полей", - "uk-ua": "Показати локалізовані підписи полів", - "pt-br": "Mostrar rótulos de campos localizados", + 'en-us': 'Show Localized Field Labels', + 'de-ch': 'Lokalisierte Feldbezeichnungen anzeigen', + 'es-es': 'Mostrar etiquetas de campos localizados', + 'fr-fr': 'Afficher les étiquettes de champ localisées', + 'ru-ru': 'Показать локализованные метки полей', + 'uk-ua': 'Показати локалізовані підписи полів', + 'pt-br': 'Mostrar rótulos de campos localizados', }, showDataModelLabels: { - "en-us": "Show Data Model Field Names", - "de-ch": "Datenmodell-Feldnamen anzeigen", - "es-es": "Mostrar nombres de campos del modelo de datos", - "fr-fr": "Afficher les noms des champs du modèle de données", - "ru-ru": "Показать имена полей модели данных", - "uk-ua": "Показати назви полів моделі даних", - "pt-br": "Mostrar nomes de campos do modelo de dados", + 'en-us': 'Show Data Model Field Names', + 'de-ch': 'Datenmodell-Feldnamen anzeigen', + 'es-es': 'Mostrar nombres de campos del modelo de datos', + 'fr-fr': 'Afficher les noms des champs du modèle de données', + 'ru-ru': 'Показать имена полей модели данных', + 'uk-ua': 'Показати назви полів моделі даних', + 'pt-br': 'Mostrar nomes de campos do modelo de dados', }, editHistory: { - "en-us": "Edit history", - "ru-ru": "История редактирования", - "es-es": "Historial de edición", - "fr-fr": "Modifier l'historique", - "uk-ua": "Історія редагування", - "de-ch": "Bearbeitungsgeschichte", - "pt-br": "Editar histórico", + 'en-us': 'Edit history', + 'ru-ru': 'История редактирования', + 'es-es': 'Historial de edición', + 'fr-fr': "Modifier l'historique", + 'uk-ua': 'Історія редагування', + 'de-ch': 'Bearbeitungsgeschichte', + 'pt-br': 'Editar histórico', }, editHistoryQueryName: { - "en-us": 'Edit history for "{formattedRecord:string}"', - "ru-ru": 'История изменений для "{formattedRecord:string}"', - "es-es": 'Historial de edición de "{formattedRecord:string}"', - "fr-fr": "Historique des modifications pour « {formattedRecord:string} »", - "uk-ua": 'Історія редагувань для "{formattedRecord:string}"', - "de-ch": "Bearbeitungsverlauf für „{formattedRecord:string}“", - "pt-br": 'Histórico de edição para "{formattedRecord:string}"', + 'en-us': 'Edit history for "{formattedRecord:string}"', + 'ru-ru': 'История изменений для "{formattedRecord:string}"', + 'es-es': 'Historial de edición de "{formattedRecord:string}"', + 'fr-fr': 'Historique des modifications pour « {formattedRecord:string} »', + 'uk-ua': 'Історія редагувань для "{formattedRecord:string}"', + 'de-ch': 'Bearbeitungsverlauf für „{formattedRecord:string}“', + 'pt-br': 'Histórico de edição para "{formattedRecord:string}"', }, formConfiguration: { - "en-us": "Form Configuration", - "ru-ru": "Конфигурация формы", - "es-es": "Configuración del formulario", - "fr-fr": "Configuration du formulaire", - "uk-ua": "Конфігурація форми", - "de-ch": "Formularkonfiguration", - "pt-br": "Configuração do formulário", + 'en-us': 'Form Configuration', + 'ru-ru': 'Конфигурация формы', + 'es-es': 'Configuración del formulario', + 'fr-fr': 'Configuration du formulaire', + 'uk-ua': 'Конфігурація форми', + 'de-ch': 'Formularkonfiguration', + 'pt-br': 'Configuração do formulário', }, formState: { - "en-us": "Form State", - "ru-ru": "Форма государства", - "es-es": "Estado del formulario", - "fr-fr": "État du formulaire", - "uk-ua": "Стан форми", - "de-ch": "Formularstatus", - "pt-br": "Estado do formulário", + 'en-us': 'Form State', + 'ru-ru': 'Форма государства', + 'es-es': 'Estado del formulario', + 'fr-fr': 'État du formulaire', + 'uk-ua': 'Стан форми', + 'de-ch': 'Formularstatus', + 'pt-br': 'Estado do formulário', }, recordInformation: { - "en-us": "Record Information", - "ru-ru": "Запись информации", - "es-es": "Información de registro", - "fr-fr": "Informations sur le dossier", - "uk-ua": "Інформація про запис", - "de-ch": "Datensatzinformationen", - "pt-br": "Informações do registro", + 'en-us': 'Record Information', + 'ru-ru': 'Запись информации', + 'es-es': 'Información de registro', + 'fr-fr': 'Informations sur le dossier', + 'uk-ua': 'Інформація про запис', + 'de-ch': 'Datensatzinformationen', + 'pt-br': 'Informações do registro', }, shareRecord: { - "en-us": "Share Record", - "ru-ru": "Поделиться записью", - "es-es": "Compartir registro", - "fr-fr": "Partager l'enregistrement", - "uk-ua": "Поділитися записом", - "de-ch": "Datensatz teilen", - "pt-br": "Compartilhar registro", + 'en-us': 'Share Record', + 'ru-ru': 'Поделиться записью', + 'es-es': 'Compartir registro', + 'fr-fr': "Partager l'enregistrement", + 'uk-ua': 'Поділитися записом', + 'de-ch': 'Datensatz teilen', + 'pt-br': 'Compartilhar registro', }, findUsages: { - "en-us": "Find usages", - "ru-ru": "Найти использование", - "es-es": "Encuentra usos", - "fr-fr": "Trouver des utilisations", - "uk-ua": "Знайти вживання", - "de-ch": "Verwendungen finden", - "pt-br": "Encontre usos", + 'en-us': 'Find usages', + 'ru-ru': 'Найти использование', + 'es-es': 'Encuentra usos', + 'fr-fr': 'Trouver des utilisations', + 'uk-ua': 'Знайти вживання', + 'de-ch': 'Verwendungen finden', + 'pt-br': 'Encontre usos', }, usagesOfPickList: { - "en-us": 'Usages of "{pickList:string}" pick list', - "ru-ru": 'Использование списка выбора "{pickList:string}"', - "es-es": 'Usos de la lista de selección "{pickList:string}"', - "fr-fr": "Utilisations de la liste de sélection « {pickList:string} »", - "uk-ua": 'Використання списку вибору "{pickList:string}"', - "de-ch": "Verwendung der Auswahlliste „{pickList:string}“", - "pt-br": 'Usos da lista de seleção "{pickList:string}"', + 'en-us': 'Usages of "{pickList:string}" pick list', + 'ru-ru': 'Использование списка выбора "{pickList:string}"', + 'es-es': 'Usos de la lista de selección "{pickList:string}"', + 'fr-fr': 'Utilisations de la liste de sélection « {pickList:string} »', + 'uk-ua': 'Використання списку вибору "{pickList:string}"', + 'de-ch': 'Verwendung der Auswahlliste „{pickList:string}“', + 'pt-br': 'Usos da lista de seleção "{pickList:string}"', }, subForm: { - "en-us": "Subform", - "ru-ru": "Подформа", - "es-es": "Subform", - "fr-fr": "Sous-formulaire", - "uk-ua": "Підформа", - "de-ch": "Unterformular", - "pt-br": "Subform", + 'en-us': 'Subform', + 'ru-ru': 'Подформа', + 'es-es': 'Subform', + 'fr-fr': 'Sous-formulaire', + 'uk-ua': 'Підформа', + 'de-ch': 'Unterformular', + 'pt-br': 'Subform', }, formTable: { - "en-us": "Grid", - "ru-ru": "Сетка", - "es-es": "Red", - "fr-fr": "Grille", - "uk-ua": "Сітка", - "de-ch": "Netz", - "pt-br": "Grade", + 'en-us': 'Grid', + 'ru-ru': 'Сетка', + 'es-es': 'Red', + 'fr-fr': 'Grille', + 'uk-ua': 'Сітка', + 'de-ch': 'Netz', + 'pt-br': 'Grade', }, subviewConfiguration: { - "en-us": "Subview", - "ru-ru": "Подвид", - "es-es": "Subvista", - "uk-ua": "Підвид", - "de-ch": "Unteransicht", - "fr-fr": "Sous-vue", - "pt-br": "Subvisualização", + 'en-us': 'Subview', + 'ru-ru': 'Подвид', + 'es-es': 'Subvista', + 'uk-ua': 'Підвид', + 'de-ch': 'Unteransicht', + 'fr-fr': 'Sous-vue', + 'pt-br': 'Subvisualização', }, disableReadOnly: { - "en-us": "Disable read-only mode", - "ru-ru": "Отключить режим только для чтения", - "es-es": "Deshabilitar el modo de solo lectura", - "fr-fr": "Désactiver le mode lecture seule", - "uk-ua": "Вимкнути режим лише для читання", - "de-ch": "Deaktivieren Sie den Nur-Lese-Modus", - "pt-br": "Desativar modo somente leitura", + 'en-us': 'Disable read-only mode', + 'ru-ru': 'Отключить режим только для чтения', + 'es-es': 'Deshabilitar el modo de solo lectura', + 'fr-fr': 'Désactiver le mode lecture seule', + 'uk-ua': 'Вимкнути режим лише для читання', + 'de-ch': 'Deaktivieren Sie den Nur-Lese-Modus', + 'pt-br': 'Desativar modo somente leitura', }, enableReadOnly: { - "en-us": "Enable read-only mode", - "ru-ru": "Включить режим только для чтения", - "es-es": "Habilitar el modo de solo lectura", - "fr-fr": "Activer le mode lecture seule", - "uk-ua": "Увімкнути режим лише для читання", - "de-ch": "Aktivieren Sie den Nur-Lese-Modus", - "pt-br": "Habilitar modo somente leitura", + 'en-us': 'Enable read-only mode', + 'ru-ru': 'Включить режим только для чтения', + 'es-es': 'Habilitar el modo de solo lectura', + 'fr-fr': 'Activer le mode lecture seule', + 'uk-ua': 'Увімкнути режим лише для читання', + 'de-ch': 'Aktivieren Sie den Nur-Lese-Modus', + 'pt-br': 'Habilitar modo somente leitura', }, configureDataEntryTables: { - "en-us": "Configure data entry tables", - "ru-ru": "Настройте таблицы ввода данных", - "es-es": "Configurar tablas de entrada de datos", - "fr-fr": "Configurer les tables de saisie de données", - "uk-ua": "Налаштування таблиць для введення даних", - "de-ch": "Konfigurieren von Dateneingabetabellen", - "pt-br": "Configurar tabelas de entrada de dados", + 'en-us': 'Configure data entry tables', + 'ru-ru': 'Настройте таблицы ввода данных', + 'es-es': 'Configurar tablas de entrada de datos', + 'fr-fr': 'Configurer les tables de saisie de données', + 'uk-ua': 'Налаштування таблиць для введення даних', + 'de-ch': 'Konfigurieren von Dateneingabetabellen', + 'pt-br': 'Configurar tabelas de entrada de dados', }, configureInteractionTables: { - "en-us": "Configure interaction tables", - "ru-ru": "Настройте таблицы взаимодействия", - "es-es": "Configurar tablas de interacción", - "fr-fr": "Configurer les tables d'interaction", - "uk-ua": "Налаштування таблиць взаємодії", - "de-ch": "Konfigurieren von Interaktionstabellen", - "pt-br": "Configurar tabelas de interação", + 'en-us': 'Configure interaction tables', + 'ru-ru': 'Настройте таблицы взаимодействия', + 'es-es': 'Configurar tablas de interacción', + 'fr-fr': "Configurer les tables d'interaction", + 'uk-ua': 'Налаштування таблиць взаємодії', + 'de-ch': 'Konfigurieren von Interaktionstabellen', + 'pt-br': 'Configurar tabelas de interação', }, formMeta: { - "en-us": "Form Meta", - "ru-ru": "Форма Мета", - "es-es": "Meta del formulario", - "fr-fr": "Formulaire Méta", - "uk-ua": "Метадані форми", - "de-ch": "Formular-Metadaten", - "pt-br": "Formulário Meta", + 'en-us': 'Form Meta', + 'ru-ru': 'Форма Мета', + 'es-es': 'Meta del formulario', + 'fr-fr': 'Formulaire Méta', + 'uk-ua': 'Метадані форми', + 'de-ch': 'Formular-Metadaten', + 'pt-br': 'Formulário Meta', }, newResourceTitle: { - "en-us": "New {tableName:string}", - "ru-ru": "Новый {tableName:string}", - "es-es": "Nuevo {tableName:string}", - "fr-fr": "Nouveau {tableName:string}", - "uk-ua": "Новий {tableName:string}", - "de-ch": "Neu {tableName:string}", - "pt-br": "Novo {tableName:string}", + 'en-us': 'New {tableName:string}', + 'ru-ru': 'Новый {tableName:string}', + 'es-es': 'Nuevo {tableName:string}', + 'fr-fr': 'Nouveau {tableName:string}', + 'uk-ua': 'Новий {tableName:string}', + 'de-ch': 'Neu {tableName:string}', + 'pt-br': 'Novo {tableName:string}', }, resourceFormatter: { comment: ` When resource does not have a formatter defined, this formatter is used `, - "en-us": "{tableName:string} #{id:number}", - "ru-ru": "{tableName:string} #{id:number}", - "es-es": "{tableName:string} #{id:number}", - "fr-fr": "{tableName:string} #{id:number}", - "uk-ua": "{tableName:string} #{id:number}", - "de-ch": "{tableName:string} #{id:number}", - "pt-br": "{tableName:string} #{id:number}", + 'en-us': '{tableName:string} #{id:number}', + 'ru-ru': '{tableName:string} #{id:number}', + 'es-es': '{tableName:string} #{id:number}', + 'fr-fr': '{tableName:string} #{id:number}', + 'uk-ua': '{tableName:string} #{id:number}', + 'de-ch': '{tableName:string} #{id:number}', + 'pt-br': '{tableName:string} #{id:number}', }, resourceDeleted: { - "en-us": "Resource deleted", - "ru-ru": "Ресурс удален", - "es-es": "Recurso eliminado", - "fr-fr": "Ressource supprimée", - "uk-ua": "Ресурс видалено", - "de-ch": "Ressource gelöscht", - "pt-br": "Recurso excluído", + 'en-us': 'Resource deleted', + 'ru-ru': 'Ресурс удален', + 'es-es': 'Recurso eliminado', + 'fr-fr': 'Ressource supprimée', + 'uk-ua': 'Ресурс видалено', + 'de-ch': 'Ressource gelöscht', + 'pt-br': 'Recurso excluído', }, resourceDeletedDescription: { - "en-us": "Item was deleted successfully.", - "ru-ru": "Элемент был успешно удален.", - "es-es": "El artículo fue eliminado exitosamente", - "fr-fr": "L'élément a été supprimé avec succès.", - "uk-ua": "Елемент успішно видалено.", - "de-ch": "Element wurde erfolgreich gelöscht.", - "pt-br": "O item foi excluído com sucesso.", + 'en-us': 'Item was deleted successfully.', + 'ru-ru': 'Элемент был успешно удален.', + 'es-es': 'El artículo fue eliminado exitosamente', + 'fr-fr': "L'élément a été supprimé avec succès.", + 'uk-ua': 'Елемент успішно видалено.', + 'de-ch': 'Element wurde erfolgreich gelöscht.', + 'pt-br': 'O item foi excluído com sucesso.', }, dateRange: { - "en-us": "(Range: {from:string} - {to:string})", - "ru-ru": "(Диапазон: {from:string} - {to:string})", - "es-es": "(Rango: {from:string} - {to:string})", - "fr-fr": "(Plage : {from:string} - {to:string})", - "uk-ua": "(Діапазон: {from:string} - {to:string})", - "de-ch": "(Bereich: {from:string} – {to:string})", - "pt-br": "(Intervalo: {from:string} - {to:string})", + 'en-us': '(Range: {from:string} - {to:string})', + 'ru-ru': '(Диапазон: {from:string} - {to:string})', + 'es-es': '(Rango: {from:string} - {to:string})', + 'fr-fr': '(Plage : {from:string} - {to:string})', + 'uk-ua': '(Діапазон: {from:string} - {to:string})', + 'de-ch': '(Bereich: {from:string} – {to:string})', + 'pt-br': '(Intervalo: {from:string} - {to:string})', }, catalogNumberNumericFormatter: { comment: 'Meaning "Catalog Number Numeric formatter"', - "en-us": "Catalog Number Numeric", - "de-ch": "Katalognummer Numerisch", - "es-es": "Número de catálogo numérico", - "fr-fr": "Numéro de catalogue numérique", - "ru-ru": "Номер по каталогу Цифровой", - "uk-ua": "Номер у каталозі (числовий)", - "pt-br": "Número de catálogo Numérico", + 'en-us': 'Catalog Number Numeric', + 'de-ch': 'Katalognummer Numerisch', + 'es-es': 'Número de catálogo numérico', + 'fr-fr': 'Numéro de catalogue numérique', + 'ru-ru': 'Номер по каталогу Цифровой', + 'uk-ua': 'Номер у каталозі (числовий)', + 'pt-br': 'Número de catálogo Numérico', }, addCOGChildren: { - "en-us": "Add COG Children", - "de-ch": "COG-Kinder hinzufügen", - "es-es": "Agregar niños COG", - "fr-fr": "Ajouter des enfants COG", - "pt-br": "Adicionar crianças COG", - "ru-ru": "Добавить детей COG", - "uk-ua": "Додати дочірні елементи COG", + 'en-us': 'Add COG Children', + 'de-ch': 'COG-Kinder hinzufügen', + 'es-es': 'Agregar niños COG', + 'fr-fr': 'Ajouter des enfants COG', + 'pt-br': 'Adicionar crianças COG', + 'ru-ru': 'Добавить детей COG', + 'uk-ua': 'Додати дочірні елементи COG', }, seriesEntry: { - "en-us": "Series Entry", + 'en-us': 'Series Entry', }, seriesEntryDescription: { - "en-us": "Create a series of new records from a range of catalog numbers", + 'en-us': 'Create a series of new records from a range of catalog numbers', }, seriesEntryStart: { - "en-us": "Series Range Start", + 'en-us': 'Series Range Start', }, seriesEntryEnd: { - "en-us": "Series Range End", + 'en-us': 'Series Range End', }, } as const); From db6f1468c48ea46dcd7fb96e83dde99506497024 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Tue, 24 Jun 2025 14:40:49 -0500 Subject: [PATCH 03/33] Increment catalog numbers on backend --- .../js_src/lib/components/Forms/Save.tsx | 115 +++++++++++------- specifyweb/specify/urls.py | 3 + specifyweb/specify/views.py | 47 +++++++ 3 files changed, 122 insertions(+), 43 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index a7e58dd8288..97f6d445c77 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -245,6 +245,77 @@ export function SaveButton({ const showSeriesEntry = true; const [seriesEntryStart, setSeriesEntryStart] = React.useState(undefined); const [seriesEntryEnd, setSeriesEntryEnd] = React.useState(undefined); + const handleSeriesEntry = (): void => { + // Scroll to the top of the form on clone + smoothScroll(form, 0); + const handleClick = async (): Promise> | undefined> => { + // Simple test, don't generate cns between start and end + const catalogNumbers = await ajax< + { readonly values: RA } + >( + `/api/specify/series_autonumber_range/`, + { + method: 'POST', + headers: { Accept: 'application/json' }, + body: { + range_start: seriesEntryStart, + range_end: seriesEntryEnd, + table_name: resource.specifyTable.name.toLowerCase(), + field_name: 'catalognumber', + }, + } + ).then(({ data }) => data.values) + .catch((error) => { + console.error(error); + return undefined; + }) + + if (catalogNumbers === undefined) { + return undefined; + } + + // Clone and set catalog numbers + const clonePromises = Array.from( + { length: catalogNumbers.length }, + async (_, index) => { + const clonedResource = await resource.clone( + false, + true + ); + clonedResource.set( + 'catalogNumber', + catalogNumbers[index] as never + ); + return clonedResource; + } + ); + + const clones = await Promise.all(clonePromises); + + const backendClones = await ajax< + RA> + >( + `/api/specify/bulk/${resource.specifyTable.name.toLowerCase()}/`, + { + method: 'POST', + headers: { Accept: 'application/json' }, + body: clones, + } + ).then(({ data }) => + data.map((resource) => + deserializeResource(serializeResource(resource)) + ) + ); + + return Promise.all([resource, ...backendClones]); + } + loading(handleClick().then((resources): void => { + if (handleAdd !== undefined && resources !== undefined) { + handleAdd(resources) + }} + ) + ); + } return ( <> @@ -279,49 +350,7 @@ export function SaveButton({ className={saveBlocked ? '!cursor-not-allowed' : undefined} disabled={isSaveDisabled || saveBlocked} title={formsText.seriesEntryDescription()} - onClick={(): void => { - // Scroll to the top of the form on clone - smoothScroll(form, 0); - const handleClick = async (): Promise>> => { - // Simple test, don't generate cns between start and end - const catalogNumbers = [seriesEntryStart, seriesEntryEnd]; - - const clonePromises = Array.from( - { length: catalogNumbers.length }, - async (_, index) => { - const clonedResource = await resource.clone( - false, - true - ); - clonedResource.set( - 'catalogNumber', - catalogNumbers[index] as never - ); - return clonedResource; - } - ); - - const clones = await Promise.all(clonePromises); - - const backendClones = await ajax< - RA> - >( - `/api/specify/bulk/${resource.specifyTable.name.toLowerCase()}/`, - { - method: 'POST', - headers: { Accept: 'application/json' }, - body: clones, - } - ).then(({ data }) => - data.map((resource) => - deserializeResource(serializeResource(resource)) - ) - ); - - return Promise.all([resource, ...backendClones]); - } - loading(handleClick().then(handleAdd)); - }} + onClick={handleSeriesEntry} > {formsText.seriesEntry()} diff --git a/specifyweb/specify/urls.py b/specifyweb/specify/urls.py index c6ee89b60ff..ebc06ca4cac 100644 --- a/specifyweb/specify/urls.py +++ b/specifyweb/specify/urls.py @@ -19,6 +19,9 @@ # cat num for parent re_path(r'^specify/catalog_number_from_parent/$', views.catalog_number_from_parent), + # retrieve auto numbered fields + re_path(r'^specify/series_autonumber_range', views.series_autonumber_range), + # check if the user is new at login re_path(r'^specify/is_new_user/$', views.is_new_user), diff --git a/specifyweb/specify/views.py b/specifyweb/specify/views.py index a8cd396b5fc..dc3c014b1f2 100644 --- a/specifyweb/specify/views.py +++ b/specifyweb/specify/views.py @@ -1499,3 +1499,50 @@ def catalog_number_from_parent(request: http.HttpRequest): except Exception as e: print(f"Error processing request: {e}") return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500) + + +from .uiformatters import UIFormatter, get_catalognumber_format, get_uiformatter +from .autonumbering import do_autonumbering + +@login_maybe_required +@require_POST +def series_autonumber_range(request: http.HttpRequest): + """ + Returns a list of autonumbered values given a range. + Used for series data entry on Collection Objects. + """ + request_data = json.loads(request.body) + range_start = request_data.get('range_start') + range_end = request_data.get('range_end') + table_name = request_data.get('table_name') + field_name = request_data.get('field_name') + + formatter = get_uiformatter(request.specify_collection, table_name, field_name) + + try: + canonicalized_range_start = formatter.canonicalize(formatter.parse(range_start)) + except: + return http.HttpResponseBadRequest('Range start does not match format.') + try: + canonicalized_range_end = formatter.canonicalize(formatter.parse(range_end)) + except: + return http.HttpResponseBadRequest('Range end does not match format.') + + try: + limit = 300 + values = [canonicalized_range_start] + previous_value = values[0] + while previous_value != canonicalized_range_end: + next_increment = ''.join(formatter.fill_vals_after(previous_value)) + values.append(next_increment) + previous_value = next_increment + if len(values) >= limit: + return http.HttpResponseBadRequest(f'Range requested exceeds limit of {limit} values.') + + logger.debug(formatter.fill_vals_after(range_start)) + + return http.JsonResponse({ + 'values': values + }) + except Exception as e: + return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500) \ No newline at end of file From 415f07fdc65ddaca767da3182bd4a110fe648818 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Tue, 24 Jun 2025 15:45:08 -0500 Subject: [PATCH 04/33] Check if catalog numbers are formatted correctly --- .../frontend/js_src/lib/components/Forms/Save.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 97f6d445c77..79144ac3072 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -243,13 +243,21 @@ export function SaveButton({ formatter === undefined; const showSeriesEntry = true; - const [seriesEntryStart, setSeriesEntryStart] = React.useState(undefined); - const [seriesEntryEnd, setSeriesEntryEnd] = React.useState(undefined); + const [seriesEntryStart, setSeriesEntryStart] = React.useState(''); + const [seriesEntryEnd, setSeriesEntryEnd] = React.useState(''); const handleSeriesEntry = (): void => { // Scroll to the top of the form on clone smoothScroll(form, 0); const handleClick = async (): Promise> | undefined> => { - // Simple test, don't generate cns between start and end + if (!formatter.parse(seriesEntryStart)) { + console.error('Please match the required format'); + return undefined; + } + if (!formatter.format(seriesEntryEnd)) { + console.error('Please match the required format'); + return undefined; + } + const catalogNumbers = await ajax< { readonly values: RA } >( From 93ddeb73dd9c60ca96fd6c8fbeacfc595b649a79 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 25 Jun 2025 10:06:30 -0500 Subject: [PATCH 05/33] Add Carry Range Preference Make range start readonly Combine with Bulk Carry Forward --- .../lib/components/FormMeta/CarryForward.tsx | 15 + .../js_src/lib/components/Forms/Save.tsx | 273 ++++++++---------- .../Preferences/UserDefinitions.tsx | 8 + .../frontend/js_src/lib/localization/forms.ts | 27 +- 4 files changed, 152 insertions(+), 171 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx b/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx index 74566398b6e..33206635ea8 100644 --- a/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx @@ -166,8 +166,14 @@ function BulkCloneConfig({ 'preferences', 'enableBukCarryForward' ); + const [globalBulkRangeEnabled, setGlobalBulkRangeEnabled] = userPreferences.use( + 'form', + 'preferences', + 'enableBulkCarryForwardRange' + ); const isBulkCarryEnabled = globalBulkEnabled.includes(table.name); + const isBulkCarryRangeEnabled = globalBulkRangeEnabled.includes(table.name); const [isOpen, handleOpen, handleClose] = useBooleanState(); @@ -189,6 +195,15 @@ function BulkCloneConfig({ {icons.cog} + + + setGlobalBulkRangeEnabled(toggleItem(globalBulkRangeEnabled, table.name)) + } + /> + {formsText.bulkCarryForwardRangeEnabled()} + {isOpen && ( ({ const loading = React.useContext(LoadingContext); const [_, setFormContext] = React.useContext(FormContext); - const { showClone, showCarry, showBulkCarry, showAdd } = + const { showClone, showCarry, showBulkCarry, showBulkCarryRange, showAdd } = useEnabledButtons(resource); const canCreate = hasTablePermission(resource.specifyTable.name, 'create'); @@ -211,7 +215,8 @@ export function SaveButton({ const copyButton = ( label: LocalizedString, description: LocalizedString, - handleClick: () => Promise>> + handleClick: () => + Promise> | undefined> | Promise>> ): JSX.Element => ( ({ onClick={(): void => { // Scroll to the top of the form on clone smoothScroll(form, 0); - loading(handleClick().then(handleAdd)); + loading( + handleClick().then((resources) => + resources && handleAdd ? handleAdd(resources) : undefined + ) + ); }} > {label} @@ -228,160 +237,133 @@ export function SaveButton({ ); const [carryForwardAmount, setCarryForwardAmount] = React.useState(1); + const [carryForwardRangeEnd, setCarryForwardRangeEnd] = + React.useState(''); const isCOGorCOJO = resource.specifyTable.name === 'CollectionObjectGroup' || resource.specifyTable.name === 'CollectionObjectGroupJoin'; // Disable bulk carry forward for COType cat num format that are undefined or one of types listed in tableValidForBulkClone() - const formatter = - tables.CollectionObject.strictGetLiteralField( - 'catalogNumber' - ).getUiFormatter(resource)!; + const numberField = + tables.CollectionObject.strictGetLiteralField('catalogNumber'); + const formatter = numberField.getUiFormatter(resource)!; const disableBulk = !tableValidForBulkClone(resource.specifyTable, resource) || formatter === undefined; - - const showSeriesEntry = true; - const [seriesEntryStart, setSeriesEntryStart] = React.useState(''); - const [seriesEntryEnd, setSeriesEntryEnd] = React.useState(''); - const handleSeriesEntry = (): void => { - // Scroll to the top of the form on clone - smoothScroll(form, 0); - const handleClick = async (): Promise> | undefined> => { - if (!formatter.parse(seriesEntryStart)) { + const parser = formatterToParser(numberField, formatter); + + const handleBulkCarryForward = async (): Promise< + RA> | undefined + > => { + const numberFieldName = 'catalogNumber'; + const wildCard = formatter.valueOrWild(); + let numbers: RA | undefined; + + if (showBulkCarryRange) { + const carryForwardRangeStart = resource.get(numberFieldName) + if (carryForwardRangeStart === null || !formatter.parse(carryForwardRangeStart)) { console.error('Please match the required format'); return undefined; } - if (!formatter.format(seriesEntryEnd)) { + if (!formatter.format(carryForwardRangeEnd)) { console.error('Please match the required format'); return undefined; } - const catalogNumbers = await ajax< - { readonly values: RA } - >( - `/api/specify/series_autonumber_range/`, - { - method: 'POST', - headers: { Accept: 'application/json' }, - body: { - range_start: seriesEntryStart, - range_end: seriesEntryEnd, - table_name: resource.specifyTable.name.toLowerCase(), - field_name: 'catalognumber', - }, - } - ).then(({ data }) => data.values) - .catch((error) => { - console.error(error); - return undefined; + numbers = await ajax<{ + readonly values: RA; + readonly existing: RA; + }>(`/api/specify/series_autonumber_range/`, { + method: 'POST', + headers: { Accept: 'application/json' }, + body: { + range_start: carryForwardRangeStart, + range_end: carryForwardRangeEnd, + table_name: resource.specifyTable.name.toLowerCase(), + field_name: numberFieldName.toLowerCase(), + }, }) + .then(({ data }) => data.values.slice(1)) + .catch((error) => { + console.error(error); + return undefined; + }); + } - if (catalogNumbers === undefined) { - return undefined; + const clonePromises = Array.from( + { length: numbers ? numbers.length : carryForwardAmount }, + async (_, index) => { + console.log(numbers ? numbers[index] : "no catalog number"); + const clonedResource = await resource.clone(false, true); + clonedResource.set( + numberFieldName, + numbers ? (numbers[index] as never) : (wildCard as never) + ); + return clonedResource; } + ); - // Clone and set catalog numbers - const clonePromises = Array.from( - { length: catalogNumbers.length }, - async (_, index) => { - const clonedResource = await resource.clone( - false, - true - ); - clonedResource.set( - 'catalogNumber', - catalogNumbers[index] as never - ); - return clonedResource; - } - ); - - const clones = await Promise.all(clonePromises); + const clones = await Promise.all(clonePromises); - const backendClones = await ajax< - RA> - >( - `/api/specify/bulk/${resource.specifyTable.name.toLowerCase()}/`, - { - method: 'POST', - headers: { Accept: 'application/json' }, - body: clones, - } - ).then(({ data }) => - data.map((resource) => - deserializeResource(serializeResource(resource)) - ) - ); - - return Promise.all([resource, ...backendClones]); - } - loading(handleClick().then((resources): void => { - if (handleAdd !== undefined && resources !== undefined) { - handleAdd(resources) - }} - ) + const backendClones = await ajax>>( + `/api/specify/bulk/${resource.specifyTable.name.toLowerCase()}/`, + { + method: 'POST', + headers: { Accept: 'application/json' }, + body: clones, + } + ).then(({ data }) => + data.map((resource) => deserializeResource(serializeResource(resource))) ); - } + + return Promise.all([resource, ...backendClones]); + }; return ( <> {typeof handleAdd === 'function' && canCreate ? ( <> - {resource.specifyTable.name === 'CollectionObject' && - (isInRecordSet === false || isInRecordSet === undefined) && - showSeriesEntry && canSave && !isCOGorCOJO - ? + {resource.specifyTable.name === 'CollectionObject' && + (isInRecordSet === false || isInRecordSet === undefined) && + isSaveDisabled && + showCarry && + showBulkCarry && + !isCOGorCOJO && + !disableBulk ? ( + showBulkCarryRange ? ( <> - setSeriesEntryStart(value) - } + value={resource.get('catalogNumber') ?? undefined} /> - setSeriesEntryEnd(value) + setCarryForwardRangeEnd(value) } /> - - {formsText.seriesEntry()} - - : undefined} - {resource.specifyTable.name === 'CollectionObject' && - (isInRecordSet === false || isInRecordSet === undefined) && - isSaveDisabled && - showCarry && - showBulkCarry && - !isCOGorCOJO && - !disableBulk ? ( - - setCarryForwardAmount(Number(value)) - } - /> + ) : ( + + setCarryForwardAmount(Number(value)) + } + /> + ) ) : null} {showCarry && !isCOGorCOJO ? copyButton( @@ -392,45 +374,10 @@ export function SaveButton({ * See https://github.com/specify/specify7/pull/4804 * */ - resource.specifyTable.name === 'CollectionObject' && + (resource.specifyTable.name === 'CollectionObject' && + showBulkCarryRange) || carryForwardAmount > 1 - ? async (): Promise>> => { - const wildCard = formatter.valueOrWild(); - - const clonePromises = Array.from( - { length: carryForwardAmount }, - async () => { - const clonedResource = await resource.clone( - false, - true - ); - clonedResource.set( - 'catalogNumber', - wildCard as never - ); - return clonedResource; - } - ); - - const clones = await Promise.all(clonePromises); - - const backendClones = await ajax< - RA> - >( - `/api/specify/bulk/${resource.specifyTable.name.toLowerCase()}/`, - { - method: 'POST', - headers: { Accept: 'application/json' }, - body: clones, - } - ).then(({ data }) => - data.map((resource) => - deserializeResource(serializeResource(resource)) - ) - ); - - return Promise.all([resource, ...backendClones]); - } + ? handleBulkCarryForward : async (): Promise>> => [ await resource.clone(false), ] @@ -525,8 +472,9 @@ function useEnabledButtons( ): { readonly showClone: boolean; readonly showCarry: boolean; - readonly showAdd: boolean; readonly showBulkCarry: boolean; + readonly showBulkCarryRange: boolean; + readonly showAdd: boolean; } { const [enableCarryForward] = userPreferences.use( 'form', @@ -538,6 +486,11 @@ function useEnabledButtons( 'preferences', 'enableBukCarryForward' ); + const [enableBulkCarryForwardRange] = userPreferences.use( + 'form', + 'preferences', + 'enableBulkCarryForwardRange' + ); const [disableClone] = userPreferences.use( 'form', 'preferences', @@ -561,6 +514,8 @@ function useEnabledButtons( enableBulkCarryForward.includes(tableName) && !NO_CLONE.has(tableName) && tableValidForBulkClone(resource.specifyTable); + const showBulkCarryRange = + showBulkCarry && enableBulkCarryForwardRange.includes(tableName); const showClone = !disableClone.includes(tableName) && !NO_CLONE.has(tableName) && @@ -568,7 +523,7 @@ function useEnabledButtons( const showAdd = !disableAdd.includes(tableName) && !FORBID_ADDING.has(tableName); - return { showClone, showCarry, showBulkCarry, showAdd }; + return { showClone, showCarry, showBulkCarry, showBulkCarryRange, showAdd }; } const appResourcesToNotClone = filterArray( diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx index 3bcf3bce76a..286010f389a 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx @@ -1201,6 +1201,14 @@ export const userPreferenceDefinitions = { renderer: f.never, container: 'div', }), + enableBulkCarryForwardRange: definePref>({ + title: localized('_enableBulkCarryForwardRange'), + requiresReload: false, + visible: false, + defaultValue: [], + renderer: f.never, + container: 'div', + }), /* * Can temporary disable clone for a given table * Since most tables are likely to have carry enabled, this pref is diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index a489f7d0794..7a3176d17ab 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -905,6 +905,21 @@ export const formsText = createDictionary({ 'de-ch': 'Dieses Feld ist erforderlich. Es muss übertragen werden', 'pt-br': 'Este campo é obrigatório. Deve ser levado adiante', }, + bulkCarryForwardRangeEnabled: { + 'en-us': 'Show Bulk Carry Forward range', + }, + bulkCarryForwardRange: { + 'en-us': 'Bulk Carry Forward range', + }, + bulkCarryForwardRangeDescription: { + 'en-us': 'Create a series of new records from a ({field:string}) range.', + }, + bulkCarryForwardRangeStart: { + 'en-us': 'Carry Forward Range Start', + }, + bulkCarryForwardRangeEnd: { + 'en-us': 'Carry Forward Range End', + }, cloneButtonEnabled: { 'en-us': 'Show Clone button', 'ru-ru': 'Показать кнопку «Клонировать»', @@ -1197,16 +1212,4 @@ export const formsText = createDictionary({ 'ru-ru': 'Добавить детей COG', 'uk-ua': 'Додати дочірні елементи COG', }, - seriesEntry: { - 'en-us': 'Series Entry', - }, - seriesEntryDescription: { - 'en-us': 'Create a series of new records from a range of catalog numbers', - }, - seriesEntryStart: { - 'en-us': 'Series Range Start', - }, - seriesEntryEnd: { - 'en-us': 'Series Range End', - }, } as const); From 99bc6c933dd187714d6f9a98d7e77064d4c079d8 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 25 Jun 2025 10:06:57 -0500 Subject: [PATCH 06/33] Add existing values response placeholder --- specifyweb/specify/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/specifyweb/specify/views.py b/specifyweb/specify/views.py index dc3c014b1f2..b4d4e48f00e 100644 --- a/specifyweb/specify/views.py +++ b/specifyweb/specify/views.py @@ -1538,11 +1538,10 @@ def series_autonumber_range(request: http.HttpRequest): previous_value = next_increment if len(values) >= limit: return http.HttpResponseBadRequest(f'Range requested exceeds limit of {limit} values.') - - logger.debug(formatter.fill_vals_after(range_start)) return http.JsonResponse({ - 'values': values + 'values': values, + 'existing': [], }) except Exception as e: return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500) \ No newline at end of file From 9604a1647945b54f9266546bcab296155731443b Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 25 Jun 2025 11:22:43 -0500 Subject: [PATCH 07/33] Validate range on backend Fix carrying forward after error occurs --- .../js_src/lib/components/Forms/Save.tsx | 16 ++++++++++------ specifyweb/specify/views.py | 16 ++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 7979947efaf..57a4ff3b1a3 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -14,13 +14,13 @@ import { formatterToParser, getValidationAttributes, } from '../../utils/parser/definitions'; -import type { RA, WritableArray } from '../../utils/types'; +import type { RA } from '../../utils/types'; import { filterArray } from '../../utils/types'; import { replaceKey } from '../../utils/utils'; import { appResourceSubTypes } from '../AppResources/types'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; -import { Input } from '../Atoms/Form'; +import { Input, Label } from '../Atoms/Form'; import { Submit } from '../Atoms/Submit'; import { LoadingContext } from '../Core/Contexts'; import type { AnySchema, SerializedRecord } from '../DataModel/helperTypes'; @@ -289,12 +289,13 @@ export function SaveButton({ console.error(error); return undefined; }); + if (numbers === undefined) + return undefined; } const clonePromises = Array.from( { length: numbers ? numbers.length : carryForwardAmount }, async (_, index) => { - console.log(numbers ? numbers[index] : "no catalog number"); const clonedResource = await resource.clone(false, true); clonedResource.set( numberFieldName, @@ -332,25 +333,28 @@ export function SaveButton({ !isCOGorCOJO && !disableBulk ? ( showBulkCarryRange ? ( - <> + + <>- setCarryForwardRangeEnd(value) } /> - + ) : ( = limit: - return http.HttpResponseBadRequest(f'Range requested exceeds limit of {limit} values.') + return http.HttpResponseBadRequest(f'Bulk carry range exceeds limit of {limit} values.') return http.JsonResponse({ 'values': values, From 0b61ce9063aa1816f7ec57d435159150c2dd494a Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 25 Jun 2025 14:11:28 -0500 Subject: [PATCH 08/33] Show dialog if error occured when carrying forward --- .../js_src/lib/components/Forms/Save.tsx | 67 +++++++++++++------ specifyweb/specify/views.py | 21 ++++-- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 57a4ff3b1a3..d5d5da03739 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -16,7 +16,7 @@ import { } from '../../utils/parser/definitions'; import type { RA } from '../../utils/types'; import { filterArray } from '../../utils/types'; -import { replaceKey } from '../../utils/utils'; +import { keysToLowerCase,replaceKey } from '../../utils/utils'; import { appResourceSubTypes } from '../AppResources/types'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; @@ -46,7 +46,6 @@ import { userPreferences } from '../Preferences/userPreferences'; import { generateMappingPathPreview } from '../WbPlanView/mappingPreview'; import { FormContext } from './BaseResourceView'; import { FORBID_ADDING, NO_CLONE } from './ResourceView'; - export const saveFormUnloadProtect = formsText.unsavedFormUnloadProtect(); /* @@ -216,7 +215,8 @@ export function SaveButton({ label: LocalizedString, description: LocalizedString, handleClick: () => - Promise> | undefined> | Promise>> + | Promise> | undefined> + | Promise>> ): JSX.Element => ( ({ formatter === undefined; const parser = formatterToParser(numberField, formatter); + const [bulkCarryBlocked, setBulkCarryBlocked] = React.useState(false); + const handleBulkCarryForward = async (): Promise< RA> | undefined > => { @@ -261,36 +263,50 @@ export function SaveButton({ let numbers: RA | undefined; if (showBulkCarryRange) { - const carryForwardRangeStart = resource.get(numberFieldName) - if (carryForwardRangeStart === null || !formatter.parse(carryForwardRangeStart)) { + const carryForwardRangeStart = resource.get(numberFieldName); + if ( + carryForwardRangeStart === null || + !formatter.parse(carryForwardRangeStart) + ) { console.error('Please match the required format'); + setBulkCarryBlocked(true); return undefined; } if (!formatter.format(carryForwardRangeEnd)) { console.error('Please match the required format'); + setBulkCarryBlocked(true); return undefined; } - numbers = await ajax<{ + const response = await ajax<{ readonly values: RA; readonly existing: RA; }>(`/api/specify/series_autonumber_range/`, { method: 'POST', headers: { Accept: 'application/json' }, - body: { - range_start: carryForwardRangeStart, - range_end: carryForwardRangeEnd, - table_name: resource.specifyTable.name.toLowerCase(), - field_name: numberFieldName.toLowerCase(), - }, + body: keysToLowerCase({ + rangeStart: carryForwardRangeStart, + rangeEnd: carryForwardRangeEnd, + tableName: resource.specifyTable.name.toLowerCase(), + fieldName: numberFieldName.toLowerCase(), + }), }) - .then(({ data }) => data.values.slice(1)) + .then(({ data }) => data) .catch((error) => { console.error(error); return undefined; }); - if (numbers === undefined) + if (response === undefined) { + setBulkCarryBlocked(true); return undefined; + } + // Ignore the first number, since that belongs to the record that was already saved + numbers = response.values.slice(1); + if (response.existing.length > 1) { + // Change this to > 0 if first value shouldn't ignored + setBulkCarryBlocked(true); + return undefined; + } } const clonePromises = Array.from( @@ -338,18 +354,17 @@ export function SaveButton({ aria-label={formsText.bulkCarryForwardRangeStart()} className="!w-fit" isReadOnly - width={numberField.datamodelDefinition.length} placeholder={formatter.valueOrWild()} value={resource.get('catalogNumber') ?? ''} + width={numberField.datamodelDefinition.length} /> - <>- setCarryForwardRangeEnd(value) } @@ -378,9 +393,8 @@ export function SaveButton({ * See https://github.com/specify/specify7/pull/4804 * */ - (resource.specifyTable.name === 'CollectionObject' && - showBulkCarryRange) || - carryForwardAmount > 1 + resource.specifyTable.name === 'CollectionObject' && + (showBulkCarryRange || carryForwardAmount > 1) ? handleBulkCarryForward : async (): Promise>> => [ await resource.clone(false), @@ -432,6 +446,19 @@ export function SaveButton({ onClose={(): void => setShownBlocker(undefined)} /> ) : undefined} + {bulkCarryBlocked ? ( + setBulkCarryBlocked(false)}> + {commonText.close()} + + } + header={formsText.carryForward()} + onClose={undefined} + > + {formsText.bulkCarryForwardErrorDescription()} + + ) : undefined} ); } diff --git a/specifyweb/specify/views.py b/specifyweb/specify/views.py index 55bddc3b75a..04762743648 100644 --- a/specifyweb/specify/views.py +++ b/specifyweb/specify/views.py @@ -20,6 +20,7 @@ from django.db.models.deletion import Collector from django.views.decorators.cache import cache_control from django.views.decorators.http import require_POST, require_http_methods +from specifyweb.specify.api import get_model from specifyweb.middleware.general import require_GET, require_http_methods from specifyweb.permissions.permissions import PermissionTarget, \ @@ -1501,8 +1502,7 @@ def catalog_number_from_parent(request: http.HttpRequest): return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500) -from .uiformatters import UIFormatter, get_catalognumber_format, get_uiformatter -from .autonumbering import do_autonumbering +from .uiformatters import get_uiformatter @login_maybe_required @require_POST @@ -1512,10 +1512,10 @@ def series_autonumber_range(request: http.HttpRequest): Used for series data entry on Collection Objects. """ request_data = json.loads(request.body) - range_start = request_data.get('range_start') - range_end = request_data.get('range_end') - table_name = request_data.get('table_name') - field_name = request_data.get('field_name') + range_start = request_data.get('rangestart') + range_end = request_data.get('rangeend') + table_name = request_data.get('tablename') + field_name = request_data.get('fieldname') formatter = get_uiformatter(request.specify_collection, table_name, field_name) @@ -1534,6 +1534,7 @@ def series_autonumber_range(request: http.HttpRequest): return http.HttpResponseBadRequest(f'Range end must be greater than range start.') try: + # Repeatedly autonumber until the end is reached. limit = 300 values = [canonicalized_range_start] current_value = values[0] @@ -1542,10 +1543,16 @@ def series_autonumber_range(request: http.HttpRequest): values.append(current_value) if len(values) >= limit: return http.HttpResponseBadRequest(f'Bulk carry range exceeds limit of {limit} values.') + + # Check if any existing records use the values. + # Not garanteed to be accurate at the time of saving, just serves as a warning for the frontend. + table = get_model(table_name) + existing_records = table.objects.filter(**{f"{field_name}__in": values}) + existing_values = list(existing_records.values_list(field_name, flat=True)) return http.JsonResponse({ 'values': values, - 'existing': [], + 'existing': existing_values, }) except Exception as e: return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500) \ No newline at end of file From bed8594506e803c2fc2c6e7e6c101b149435d700 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 25 Jun 2025 15:15:13 -0500 Subject: [PATCH 09/33] Show dialog for cat nums already in use --- .../js_src/lib/components/Forms/Save.tsx | 31 ++++++++++++------- .../frontend/js_src/lib/localization/forms.ts | 7 +++-- specifyweb/specify/views.py | 4 +++ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index d5d5da03739..f82200bb701 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -253,7 +253,8 @@ export function SaveButton({ formatter === undefined; const parser = formatterToParser(numberField, formatter); - const [bulkCarryBlocked, setBulkCarryBlocked] = React.useState(false); + const [bulkCarryRangeBlocked, setBulkCarryRangeBlocked] = React.useState(false); + const [bulkCarryRangeInvalidNumbers, setBulkCarryRangeInvalidNumbers] = React.useState | undefined>(undefined); const handleBulkCarryForward = async (): Promise< RA> | undefined @@ -269,12 +270,12 @@ export function SaveButton({ !formatter.parse(carryForwardRangeStart) ) { console.error('Please match the required format'); - setBulkCarryBlocked(true); + setBulkCarryRangeBlocked(true); return undefined; } if (!formatter.format(carryForwardRangeEnd)) { console.error('Please match the required format'); - setBulkCarryBlocked(true); + setBulkCarryRangeBlocked(true); return undefined; } @@ -289,6 +290,7 @@ export function SaveButton({ rangeEnd: carryForwardRangeEnd, tableName: resource.specifyTable.name.toLowerCase(), fieldName: numberFieldName.toLowerCase(), + skipStartNumber: true, }), }) .then(({ data }) => data) @@ -297,14 +299,13 @@ export function SaveButton({ return undefined; }); if (response === undefined) { - setBulkCarryBlocked(true); + setBulkCarryRangeBlocked(true); return undefined; } - // Ignore the first number, since that belongs to the record that was already saved - numbers = response.values.slice(1); - if (response.existing.length > 1) { - // Change this to > 0 if first value shouldn't ignored - setBulkCarryBlocked(true); + numbers = response.values; + if (response.existing.length > 0) { + setBulkCarryRangeInvalidNumbers(response.existing); + setBulkCarryRangeBlocked(true); return undefined; } } @@ -446,17 +447,23 @@ export function SaveButton({ onClose={(): void => setShownBlocker(undefined)} /> ) : undefined} - {bulkCarryBlocked ? ( + {bulkCarryRangeBlocked ? ( setBulkCarryBlocked(false)}> + {setBulkCarryRangeBlocked(false); setBulkCarryRangeInvalidNumbers(undefined)}}> {commonText.close()} } header={formsText.carryForward()} onClose={undefined} > - {formsText.bulkCarryForwardErrorDescription()} + {bulkCarryRangeInvalidNumbers !== undefined ? + (<>{formsText.bulkCarryForwardRangeExistingRecords({field: numberField.label})} + {bulkCarryRangeInvalidNumbers.map((number, index) => ( +

{number}

+ ))}) + : formsText.bulkCarryForwardRangeErrorDescription({field: numberField.label}) + }
) : undefined} diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 7a3176d17ab..51d47dddcd3 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -911,8 +911,11 @@ export const formsText = createDictionary({ bulkCarryForwardRange: { 'en-us': 'Bulk Carry Forward range', }, - bulkCarryForwardRangeDescription: { - 'en-us': 'Create a series of new records from a ({field:string}) range.', + bulkCarryForwardRangeErrorDescription: { + 'en-us': 'Cannot carry forward record through the specified {field:string} range.', + }, + bulkCarryForwardRangeExistingRecords: { + 'en-us': 'The following numbers for {field:string} are already being used:', }, bulkCarryForwardRangeStart: { 'en-us': 'Carry Forward Range Start', diff --git a/specifyweb/specify/views.py b/specifyweb/specify/views.py index 04762743648..db8f897d665 100644 --- a/specifyweb/specify/views.py +++ b/specifyweb/specify/views.py @@ -1538,6 +1538,10 @@ def series_autonumber_range(request: http.HttpRequest): limit = 300 values = [canonicalized_range_start] current_value = values[0] + if request_data.get('skipstartnumber'): + # The first value can be optionally excluded/skipped. + # Needed since series entry currently relies on the first record being saved first. + values = [] while current_value < canonicalized_range_end: current_value = ''.join(formatter.fill_vals_after(current_value)) values.append(current_value) From d3cd67ef900ea5a06995a76cf7b323d6b85f0be1 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 09:23:02 -0500 Subject: [PATCH 10/33] Make bulk range pref separate --- .../lib/components/FormMeta/CarryForward.tsx | 34 +++++++---- .../js_src/lib/components/Forms/Save.tsx | 61 +++++++++++++------ 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx b/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx index 33206635ea8..baa6220fc01 100644 --- a/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx @@ -166,11 +166,8 @@ function BulkCloneConfig({ 'preferences', 'enableBukCarryForward' ); - const [globalBulkRangeEnabled, setGlobalBulkRangeEnabled] = userPreferences.use( - 'form', - 'preferences', - 'enableBulkCarryForwardRange' - ); + const [globalBulkRangeEnabled, setGlobalBulkRangeEnabled] = + userPreferences.use('form', 'preferences', 'enableBulkCarryForwardRange'); const isBulkCarryEnabled = globalBulkEnabled.includes(table.name); const isBulkCarryRangeEnabled = globalBulkRangeEnabled.includes(table.name); @@ -182,9 +179,12 @@ function BulkCloneConfig({ - setGlobalBulkEnabled(toggleItem(globalBulkEnabled, table.name)) - } + onChange={(): void => { + setGlobalBulkEnabled(toggleItem(globalBulkEnabled, table.name)); + setGlobalBulkRangeEnabled( + globalBulkRangeEnabled.filter((name) => name !== table.name) + ); + }} /> {formsText.bulkCarryForwardEnabled()} - setGlobalBulkRangeEnabled(toggleItem(globalBulkRangeEnabled, table.name)) - } + onChange={(): void => { + setGlobalBulkRangeEnabled( + toggleItem(globalBulkRangeEnabled, table.name) + ); + setGlobalBulkEnabled( + globalBulkEnabled.filter((name) => name !== table.name) + ); + }} /> {formsText.bulkCarryForwardRangeEnabled()} + + {icons.cog} + {isOpen && ( ({ const loading = React.useContext(LoadingContext); const [_, setFormContext] = React.useContext(FormContext); - const { showClone, showCarry, showBulkCarry, showBulkCarryRange, showAdd } = - useEnabledButtons(resource); + const { + showClone, + showCarry, + showBulkCarryCount, + showBulkCarryRange, + showAdd, + } = useEnabledButtons(resource); const canCreate = hasTablePermission(resource.specifyTable.name, 'create'); const canUpdate = hasTablePermission(resource.specifyTable.name, 'update'); @@ -253,8 +258,10 @@ export function SaveButton({ formatter === undefined; const parser = formatterToParser(numberField, formatter); - const [bulkCarryRangeBlocked, setBulkCarryRangeBlocked] = React.useState(false); - const [bulkCarryRangeInvalidNumbers, setBulkCarryRangeInvalidNumbers] = React.useState | undefined>(undefined); + const [bulkCarryRangeBlocked, setBulkCarryRangeBlocked] = + React.useState(false); + const [bulkCarryRangeInvalidNumbers, setBulkCarryRangeInvalidNumbers] = + React.useState | undefined>(undefined); const handleBulkCarryForward = async (): Promise< RA> | undefined @@ -269,12 +276,10 @@ export function SaveButton({ carryForwardRangeStart === null || !formatter.parse(carryForwardRangeStart) ) { - console.error('Please match the required format'); setBulkCarryRangeBlocked(true); return undefined; } if (!formatter.format(carryForwardRangeEnd)) { - console.error('Please match the required format'); setBulkCarryRangeBlocked(true); return undefined; } @@ -346,7 +351,7 @@ export function SaveButton({ (isInRecordSet === false || isInRecordSet === undefined) && isSaveDisabled && showCarry && - showBulkCarry && + (showBulkCarryCount || showBulkCarryRange) && !isCOGorCOJO && !disableBulk ? ( showBulkCarryRange ? ( @@ -450,20 +455,32 @@ export function SaveButton({ {bulkCarryRangeBlocked ? ( {setBulkCarryRangeBlocked(false); setBulkCarryRangeInvalidNumbers(undefined)}}> + { + setBulkCarryRangeBlocked(false); + setBulkCarryRangeInvalidNumbers(undefined); + }} + > {commonText.close()} } header={formsText.carryForward()} onClose={undefined} > - {bulkCarryRangeInvalidNumbers !== undefined ? - (<>{formsText.bulkCarryForwardRangeExistingRecords({field: numberField.label})} + {bulkCarryRangeInvalidNumbers !== undefined ? ( + <> + {formsText.bulkCarryForwardRangeExistingRecords({ + field: numberField.label, + })} {bulkCarryRangeInvalidNumbers.map((number, index) => (

{number}

- ))}) - : formsText.bulkCarryForwardRangeErrorDescription({field: numberField.label}) - } + ))} + + ) : ( + formsText.bulkCarryForwardRangeErrorDescription({ + field: numberField.label, + }) + )}
) : undefined} @@ -510,7 +527,7 @@ function useEnabledButtons( ): { readonly showClone: boolean; readonly showCarry: boolean; - readonly showBulkCarry: boolean; + readonly showBulkCarryCount: boolean; readonly showBulkCarryRange: boolean; readonly showAdd: boolean; } { @@ -549,9 +566,9 @@ function useEnabledButtons( const showCarry = enableCarryForward.includes(tableName) && !NO_CLONE.has(tableName); const showBulkCarry = - enableBulkCarryForward.includes(tableName) && - !NO_CLONE.has(tableName) && - tableValidForBulkClone(resource.specifyTable); + !NO_CLONE.has(tableName) && tableValidForBulkClone(resource.specifyTable); + const showBulkCarryCount = + showBulkCarry && enableBulkCarryForward.includes(tableName); const showBulkCarryRange = showBulkCarry && enableBulkCarryForwardRange.includes(tableName); const showClone = @@ -561,7 +578,13 @@ function useEnabledButtons( const showAdd = !disableAdd.includes(tableName) && !FORBID_ADDING.has(tableName); - return { showClone, showCarry, showBulkCarry, showBulkCarryRange, showAdd }; + return { + showClone, + showCarry, + showBulkCarryCount, + showBulkCarryRange, + showAdd, + }; } const appResourcesToNotClone = filterArray( From 2f592f82e592d629505e76bcb2b8199bcc93f1ca Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 09:48:34 -0500 Subject: [PATCH 11/33] Validate range start({ const carryForwardRangeStart = resource.get(numberFieldName); if ( carryForwardRangeStart === null || - !formatter.parse(carryForwardRangeStart) + !formatter.format(carryForwardRangeStart) || + !formatter.format(carryForwardRangeEnd) || + (formatter.format(carryForwardRangeStart) ?? '') > + (formatter.format(carryForwardRangeEnd) ?? '') ) { setBulkCarryRangeBlocked(true); return undefined; } - if (!formatter.format(carryForwardRangeEnd)) { - setBulkCarryRangeBlocked(true); - return undefined; - } const response = await ajax<{ readonly values: RA; diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 51d47dddcd3..468cf3a1287 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -908,9 +908,6 @@ export const formsText = createDictionary({ bulkCarryForwardRangeEnabled: { 'en-us': 'Show Bulk Carry Forward range', }, - bulkCarryForwardRange: { - 'en-us': 'Bulk Carry Forward range', - }, bulkCarryForwardRangeErrorDescription: { 'en-us': 'Cannot carry forward record through the specified {field:string} range.', }, From 69894c3d77f03c82c439a5f04ccbd3a2c85e7993 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 14:52:37 +0000 Subject: [PATCH 12/33] Lint code with ESLint and Prettier Triggered by 2f592f82e592d629505e76bcb2b8199bcc93f1ca on branch refs/heads/issue-6276 --- .../frontend/js_src/lib/components/Forms/Save.tsx | 10 +++++----- specifyweb/frontend/js_src/lib/localization/forms.ts | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 9930f68b7c2..8dbae2610db 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -466,7 +466,11 @@ export function SaveButton({ header={formsText.carryForward()} onClose={undefined} > - {bulkCarryRangeInvalidNumbers !== undefined ? ( + {bulkCarryRangeInvalidNumbers === undefined ? ( + formsText.bulkCarryForwardRangeErrorDescription({ + field: numberField.label, + }) + ) : ( <> {formsText.bulkCarryForwardRangeExistingRecords({ field: numberField.label, @@ -475,10 +479,6 @@ export function SaveButton({

{number}

))} - ) : ( - formsText.bulkCarryForwardRangeErrorDescription({ - field: numberField.label, - }) )} ) : undefined} diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 468cf3a1287..3dc4c61dda4 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -909,7 +909,8 @@ export const formsText = createDictionary({ 'en-us': 'Show Bulk Carry Forward range', }, bulkCarryForwardRangeErrorDescription: { - 'en-us': 'Cannot carry forward record through the specified {field:string} range.', + 'en-us': + 'Cannot carry forward record through the specified {field:string} range.', }, bulkCarryForwardRangeExistingRecords: { 'en-us': 'The following numbers for {field:string} are already being used:', From e681fa5fda45cc3ecf30f60b45f5fec5b2897093 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 11:43:24 -0500 Subject: [PATCH 13/33] Add "createRecordSetOnCarry" preference Separate Bulk Carry Dialog --- .../lib/components/FormMeta/CarryForward.tsx | 14 ++++++ .../lib/components/FormSliders/RecordSet.tsx | 14 +++++- .../lib/components/Forms/BulkCarryForward.tsx | 45 +++++++++++++++++ .../js_src/lib/components/Forms/Save.tsx | 48 ++++++------------- .../Preferences/UserDefinitions.tsx | 8 ++++ .../frontend/js_src/lib/localization/forms.ts | 3 ++ 6 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx diff --git a/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx b/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx index baa6220fc01..9993a92b4f3 100644 --- a/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormMeta/CarryForward.tsx @@ -168,9 +168,12 @@ function BulkCloneConfig({ ); const [globalBulkRangeEnabled, setGlobalBulkRangeEnabled] = userPreferences.use('form', 'preferences', 'enableBulkCarryForwardRange'); + const [globalCreateRecordSetOnBulkCarryForward, setGlobalCreateRecordSetOnBulkCarryForward] = + userPreferences.use('form', 'preferences', 'createRecordSetOnBulkCarryForward'); const isBulkCarryEnabled = globalBulkEnabled.includes(table.name); const isBulkCarryRangeEnabled = globalBulkRangeEnabled.includes(table.name); + const createRecordSetOnBulkCarryForward = globalCreateRecordSetOnBulkCarryForward.includes(table.name); const [isOpen, handleOpen, handleClose] = useBooleanState(); @@ -216,6 +219,17 @@ function BulkCloneConfig({ {icons.cog} + + { + setGlobalCreateRecordSetOnBulkCarryForward( + toggleItem(globalCreateRecordSetOnBulkCarryForward, table.name) + ); + }} + /> + {formsText.createRecordSetOnBulkCarryForward()} + {isOpen && ( ({ const [openDialogForTitle, _, __, setOpenDialogForTitle] = useBooleanState(false); + const [createRecordSetOnBulkCarryForward] = userPreferences.use( + 'form', + 'preferences', + 'createRecordSetOnBulkCarryForward' + ); + return ( <> @@ -419,7 +426,12 @@ function RecordSet({ } onClone={(resources: RA>): void => { go(totalCount, 'new', resources[0]); - if (resources.length > 1) { + if ( + createRecordSetOnBulkCarryForward.includes( + resources[0].specifyTable.name + ) && + resources.length > 1 + ) { const sortedResources = Array.from(resources).sort( sortFunction((r) => r.id) ) as RA>; diff --git a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx new file mode 100644 index 00000000000..edb61fc2c56 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { commonText } from '../../localization/common'; +import { formsText } from '../../localization/forms'; +import type { RA } from '../../utils/types'; +import { Button } from '../Atoms/Button'; +import type { LiteralField } from '../DataModel/specifyField'; +import { Dialog } from '../Molecules/Dialog'; + +export function BulkCarryRangeBlockedDialog({ + invalidNumbers, + numberField, + onClose: handleClose, +}: { + readonly invalidNumbers: RA | undefined; + readonly numberField: LiteralField; + readonly onClose: () => void; +}): JSX.Element { + return ( + + {commonText.close()} + + } + header={formsText.carryForward()} + onClose={undefined} + > + {invalidNumbers === undefined ? ( + formsText.bulkCarryForwardRangeErrorDescription({ + field: numberField.label, + }) + ) : ( + <> + {formsText.bulkCarryForwardRangeExistingRecords({ + field: numberField.label, + })} + {invalidNumbers.map((number, index) => ( +

{number}

+ ))} + + )} +
+ ); +} diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 8dbae2610db..04f70f05f16 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -46,6 +46,7 @@ import { userPreferences } from '../Preferences/userPreferences'; import { generateMappingPathPreview } from '../WbPlanView/mappingPreview'; import { FormContext } from './BaseResourceView'; import { FORBID_ADDING, NO_CLONE } from './ResourceView'; +import { BulkCarryRangeBlockedDialog } from './BulkCarryForward'; export const saveFormUnloadProtect = formsText.unsavedFormUnloadProtect(); /* @@ -231,8 +232,10 @@ export function SaveButton({ // Scroll to the top of the form on clone smoothScroll(form, 0); loading( - handleClick().then((resources) => - resources && handleAdd ? handleAdd(resources) : undefined + handleClick().then((resources) => { + console.log(resources); + return resources && handleAdd ? handleAdd(resources) : undefined; + } ) ); }} @@ -261,7 +264,7 @@ export function SaveButton({ const [bulkCarryRangeBlocked, setBulkCarryRangeBlocked] = React.useState(false); const [bulkCarryRangeInvalidNumbers, setBulkCarryRangeInvalidNumbers] = - React.useState | undefined>(undefined); + React.useState | undefined>(undefined); const handleBulkCarryForward = async (): Promise< RA> | undefined @@ -285,7 +288,7 @@ export function SaveButton({ const response = await ajax<{ readonly values: RA; - readonly existing: RA; + readonly existing: RA; }>(`/api/specify/series_autonumber_range/`, { method: 'POST', headers: { Accept: 'application/json' }, @@ -452,35 +455,14 @@ export function SaveButton({ /> ) : undefined} {bulkCarryRangeBlocked ? ( - { - setBulkCarryRangeBlocked(false); - setBulkCarryRangeInvalidNumbers(undefined); - }} - > - {commonText.close()} - - } - header={formsText.carryForward()} - onClose={undefined} - > - {bulkCarryRangeInvalidNumbers === undefined ? ( - formsText.bulkCarryForwardRangeErrorDescription({ - field: numberField.label, - }) - ) : ( - <> - {formsText.bulkCarryForwardRangeExistingRecords({ - field: numberField.label, - })} - {bulkCarryRangeInvalidNumbers.map((number, index) => ( -

{number}

- ))} - - )} -
+ { + setBulkCarryRangeBlocked(false); + setBulkCarryRangeInvalidNumbers(undefined); + }} + /> ) : undefined} ); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx index e7ea4bcb62c..82483c5819c 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/UserDefinitions.tsx @@ -1209,6 +1209,14 @@ export const userPreferenceDefinitions = { renderer: f.never, container: 'div', }), + createRecordSetOnBulkCarryForward: definePref>({ + title: localized('_createRecordSetOnBulkCarryForward'), + requiresReload: false, + visible: false, + defaultValue: [], + renderer: f.never, + container: 'div', + }), /* * Can temporary disable clone for a given table * Since most tables are likely to have carry enabled, this pref is diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 3dc4c61dda4..ecdab84a077 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -921,6 +921,9 @@ export const formsText = createDictionary({ bulkCarryForwardRangeEnd: { 'en-us': 'Carry Forward Range End', }, + createRecordSetOnBulkCarryForward: { + 'en-us': 'Create record set on Bulk Carry Forward' + }, cloneButtonEnabled: { 'en-us': 'Show Clone button', 'ru-ru': 'Показать кнопку «Клонировать»', From 5dccd60a8f13cfa5cfde0a70b6903064a8256e50 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 16:47:26 +0000 Subject: [PATCH 14/33] Lint code with ESLint and Prettier Triggered by e681fa5fda45cc3ecf30f60b45f5fec5b2897093 on branch refs/heads/issue-6276 --- specifyweb/frontend/js_src/lib/components/Forms/Save.tsx | 9 +++++---- specifyweb/frontend/js_src/lib/localization/forms.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 04f70f05f16..5fa5b85a7c9 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -45,8 +45,8 @@ import { hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import { generateMappingPathPreview } from '../WbPlanView/mappingPreview'; import { FormContext } from './BaseResourceView'; -import { FORBID_ADDING, NO_CLONE } from './ResourceView'; import { BulkCarryRangeBlockedDialog } from './BulkCarryForward'; +import { FORBID_ADDING, NO_CLONE } from './ResourceView'; export const saveFormUnloadProtect = formsText.unsavedFormUnloadProtect(); /* @@ -234,9 +234,10 @@ export function SaveButton({ loading( handleClick().then((resources) => { console.log(resources); - return resources && handleAdd ? handleAdd(resources) : undefined; - } - ) + return resources && handleAdd + ? void handleAdd(resources) + : undefined; + }) ); }} > diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index ecdab84a077..29503075f8e 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -922,7 +922,7 @@ export const formsText = createDictionary({ 'en-us': 'Carry Forward Range End', }, createRecordSetOnBulkCarryForward: { - 'en-us': 'Create record set on Bulk Carry Forward' + 'en-us': 'Create record set on Bulk Carry Forward', }, cloneButtonEnabled: { 'en-us': 'Show Clone button', From 30e066acfabe6aad7527c8f1bab5cd23bd7cea0d Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 12:16:43 -0500 Subject: [PATCH 15/33] Show Bulk Carry results when recordset is disabled --- .../lib/components/FormSliders/RecordSet.tsx | 54 ++++++++++--------- .../js_src/lib/components/Forms/Save.tsx | 8 +-- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx index 2d99ddcd486..443b81d7531 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx @@ -425,33 +425,37 @@ function RecordSet({ : undefined } onClone={(resources: RA>): void => { + console.log(resources); go(totalCount, 'new', resources[0]); - if ( - createRecordSetOnBulkCarryForward.includes( + if (resources.length > 1) { + if (createRecordSetOnBulkCarryForward.includes( resources[0].specifyTable.name - ) && - resources.length > 1 - ) { - const sortedResources = Array.from(resources).sort( - sortFunction((r) => r.id) - ) as RA>; - loading( - createNewRecordSet( - sortedResources.map((resource) => resource.id) - ).then(async () => { - const firstCollectionObject = await format(sortedResources[0]); - const lastCollectionObject = await format( - sortedResources.at(-1) - ); - recordSet.set( - 'name', - `${ - tables.CollectionObject.label - } Batch ${firstCollectionObject!} - ${lastCollectionObject!}` - ); - await recordSet.save(); - }) - ); + )) { + const sortedResources = Array.from(resources).sort( + sortFunction((r) => r.id) + ) as RA>; + loading( + createNewRecordSet( + sortedResources.map((resource) => resource.id) + ).then(async () => { + const firstCollectionObject = await format(sortedResources[0]); + const lastCollectionObject = await format( + sortedResources.at(-1) + ); + recordSet.set( + 'name', + `${ + tables.CollectionObject.label + } Batch ${firstCollectionObject!} - ${lastCollectionObject!}` + ); + await recordSet.save(); + }) + ); + } else { + // Don't create new record set + go(totalCount, resources[resources.length-1].id); + handleAdd(resources.slice(1), false); + } } }} onClose={handleClose} diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 04f70f05f16..70b37d6b41e 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -232,11 +232,7 @@ export function SaveButton({ // Scroll to the top of the form on clone smoothScroll(form, 0); loading( - handleClick().then((resources) => { - console.log(resources); - return resources && handleAdd ? handleAdd(resources) : undefined; - } - ) + handleClick().then((resources) => resources && handleAdd ? handleAdd(resources) : undefined) ); }} > @@ -279,7 +275,7 @@ export function SaveButton({ carryForwardRangeStart === null || !formatter.format(carryForwardRangeStart) || !formatter.format(carryForwardRangeEnd) || - (formatter.format(carryForwardRangeStart) ?? '') > + (formatter.format(carryForwardRangeStart) ?? '') >= (formatter.format(carryForwardRangeEnd) ?? '') ) { setBulkCarryRangeBlocked(true); From 77b5ae53bfe585049243847de766ad2ef664f3bb Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 17:21:04 +0000 Subject: [PATCH 16/33] Lint code with ESLint and Prettier Triggered by 5e89f5441280a00bb6f4cc6e943ffc02471a3c2b on branch refs/heads/issue-6276 --- .../frontend/js_src/lib/components/FormSliders/RecordSet.tsx | 2 +- specifyweb/frontend/js_src/lib/components/Forms/Save.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx index 443b81d7531..cbb2fb82eda 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx @@ -453,7 +453,7 @@ function RecordSet({ ); } else { // Don't create new record set - go(totalCount, resources[resources.length-1].id); + go(totalCount, resources.at(-1).id); handleAdd(resources.slice(1), false); } } diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 18127dcc6db..f88191717f5 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -232,7 +232,9 @@ export function SaveButton({ // Scroll to the top of the form on clone smoothScroll(form, 0); loading( - handleClick().then((resources) => resources && handleAdd ? handleAdd(resources) : undefined) + handleClick().then((resources) => + resources && handleAdd ? handleAdd(resources) : undefined + ) ); }} > From 0825d72a037dc212af4523c7d91dc3d7ed55fb97 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Mon, 30 Jun 2025 14:36:51 -0500 Subject: [PATCH 17/33] Add tests Fix wildcard detection --- .../specify/tests/test_series_autonumber.py | 150 ++++++++++++++++++ specifyweb/specify/views.py | 10 +- 2 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 specifyweb/specify/tests/test_series_autonumber.py diff --git a/specifyweb/specify/tests/test_series_autonumber.py b/specifyweb/specify/tests/test_series_autonumber.py new file mode 100644 index 00000000000..9c66ad3751b --- /dev/null +++ b/specifyweb/specify/tests/test_series_autonumber.py @@ -0,0 +1,150 @@ +import json +from django.test import Client +from specifyweb.specify.tests.test_api import ApiTests +from specifyweb.specify.models import (Collectionobject) + +class TestSeriesAutonumber(ApiTests): + + def test_series_autonumber_ranges(self): + c = Client() + c.force_login(self.specifyuser) + + self.collection.catalognumformatname = "CatalogNumberNumeric" + self.collection.save() + + # 000000005 to 000000007 + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '5', + 'rangeend': '7', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': False, + }), + content_type="application/json", + ) + content = json.loads(response.content.decode()) + self.assertEqual(content['values'], ["000000005","000000006","000000007"]) + + # 000000010 to 000000015, skipping the first value + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '10', + 'rangeend': '15', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': True, + }), + content_type="application/json", + ) + content = json.loads(response.content.decode()) + self.assertEqual(content['values'], ["000000011","000000012","000000013","000000014","000000015"]) + + # Range start value must be less than range end value + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '10', + 'rangeend': '0', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': True + }), + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + + # Range cannot include wildcards + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '#########', + 'rangeend': '10', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': True, + }), + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + + # Test range limit + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '0', + 'rangeend': '1000', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': True, + }), + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + + # Test autonumbering with CatalogNumberAlphaNumByYear format + self.collection.catalognumformatname ="CatalogNumberAlphaNumByYear" + self.collection.save() + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '2023-000001', + 'rangeend': '2023-000005', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': False, + }), + content_type="application/json", + ) + content = json.loads(response.content.decode()) + self.assertEqual(content['values'], ["2023-000001","2023-000002","2023-000003","2023-000004","2023-000005"]) + + def test_series_autonumber_existing(self): + c = Client() + c.force_login(self.specifyuser) + + self.collection.catalognumformatname ="CatalogNumberNumeric" + self.collection.save() + + Collectionobject.objects.create( + collection=self.collection, + catalognumber='000000012' + ) + Collectionobject.objects.create( + collection=self.collection, + catalognumber='000000013' + ) + Collectionobject.objects.create( + collection=self.collection, + catalognumber='000000014' + ) + + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '10', + 'rangeend': '20', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': True, + }), + content_type="application/json", + ) + content = json.loads(response.content.decode()) + self.assertEqual(content['existing'], ['000000012','000000013','000000014']) + + response = c.post( + f"/api/specify/series_autonumber_range/", + data=json.dumps({ + 'rangestart': '0', + 'rangeend': '10', + 'tablename': 'collectionobject', + 'fieldname': 'catalognumber', + 'skipstartnumber': True, + }), + content_type="application/json", + ) + content = json.loads(response.content.decode()) + self.assertEqual(content['existing'], []) \ No newline at end of file diff --git a/specifyweb/specify/views.py b/specifyweb/specify/views.py index db8f897d665..d34390f1fbb 100644 --- a/specifyweb/specify/views.py +++ b/specifyweb/specify/views.py @@ -1520,13 +1520,15 @@ def series_autonumber_range(request: http.HttpRequest): formatter = get_uiformatter(request.specify_collection, table_name, field_name) try: - canonicalized_range_start = formatter.canonicalize(formatter.parse(range_start)) - assert not formatter.needs_autonumber(canonicalized_range_start) + range_start_parsed = formatter.parse(range_start) + assert not formatter.needs_autonumber(range_start_parsed) + canonicalized_range_start = formatter.canonicalize(range_start_parsed) except: return http.HttpResponseBadRequest('Range start does not match format.') try: - canonicalized_range_end = formatter.canonicalize(formatter.parse(range_end)) - assert not formatter.needs_autonumber(canonicalized_range_end) + range_end_parsed = formatter.parse(range_end) + assert not formatter.needs_autonumber(range_end_parsed) + canonicalized_range_end = formatter.canonicalize(range_end_parsed) except: return http.HttpResponseBadRequest('Range end does not match format.') From 3848d8a440d7e1dca3fe8c867d7b0fb322c9855e Mon Sep 17 00:00:00 2001 From: alesan99 Date: Tue, 1 Jul 2025 11:09:37 -0500 Subject: [PATCH 18/33] Fix not using COT's cn formatter Disable Carry Forward when no auto increment Add canAutoIncrement method to formatters --- .../lib/components/FieldFormatters/index.ts | 6 +- .../lib/components/FormEditor/viewSpec.ts | 7 ++- .../js_src/lib/components/Forms/Save.tsx | 55 +++++++++++-------- .../specify/tests/test_series_autonumber.py | 13 +++-- specifyweb/specify/views.py | 6 +- 5 files changed, 55 insertions(+), 32 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts index 50357e95fb2..2e2310845cb 100644 --- a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts +++ b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts @@ -97,6 +97,10 @@ export class UiFormatter { return this.fields.some((field) => field.canAutonumber()); } + public canAutoIncrement(): boolean { + return this.fields.some((field) => field.autoIncrement); + } + public format(value: string): LocalizedString | undefined { const parsed = this.parse(value); return parsed === undefined ? undefined : this.canonicalize(parsed); @@ -122,7 +126,7 @@ abstract class Field { public readonly value: LocalizedString; - private readonly autoIncrement: boolean; + public readonly autoIncrement: boolean; private readonly byYear: boolean; diff --git a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts index 4a822a4af41..8a3095debe2 100644 --- a/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts +++ b/specifyweb/frontend/js_src/lib/components/FormEditor/viewSpec.ts @@ -712,6 +712,11 @@ const rawFieldSpec = (table: SpecifyTable | undefined) => dataObjectFormatter: syncers.xmlAttribute('formatName', 'skip'), // Example: CatalogNumberNumeric uiFieldFormatter: syncers.xmlAttribute('uiFieldFormatter', 'skip'), + series: pipe( + syncers.xmlAttribute('initialize series', 'skip'), + syncers.maybe(syncers.toBoolean), + syncers.default(false) + ), rest: syncers.captureLogContext(), }); @@ -771,7 +776,7 @@ const textSpec = f.store(() => * This is either for series data entry, or for displaying catalog number * field as separate inputs (one for each part of the formatter) */ - legacyIsSeries: pipe( + isSeries: pipe( syncers.xmlAttribute('initialize series', 'skip'), syncers.maybe(syncers.toBoolean), syncers.default(false) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index f88191717f5..b6a11e49351 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -220,13 +220,14 @@ export function SaveButton({ const copyButton = ( label: LocalizedString, description: LocalizedString, + disabled: boolean, handleClick: () => | Promise> | undefined> | Promise>> ): JSX.Element => ( { // Scroll to the top of the form on clone @@ -257,6 +258,7 @@ export function SaveButton({ const disableBulk = !tableValidForBulkClone(resource.specifyTable, resource) || formatter === undefined; + const canAutoNumberFormatter = formatter.canAutoIncrement(); const parser = formatterToParser(numberField, formatter); const [bulkCarryRangeBlocked, setBulkCarryRangeBlocked] = @@ -295,6 +297,7 @@ export function SaveButton({ rangeEnd: carryForwardRangeEnd, tableName: resource.specifyTable.name.toLowerCase(), fieldName: numberFieldName.toLowerCase(), + formatterName: formatter.title, skipStartNumber: true, }), }) @@ -355,27 +358,29 @@ export function SaveButton({ !isCOGorCOJO && !disableBulk ? ( showBulkCarryRange ? ( - - - - setCarryForwardRangeEnd(value) - } - /> - + canAutoNumberFormatter ? ( + + + + setCarryForwardRangeEnd(value) + } + /> + + ) : undefined ) : ( ({ * See https://github.com/specify/specify7/pull/4804 * */ + !( + !showBulkCarryRange || + (showBulkCarryRange && canAutoNumberFormatter) + ), resource.specifyTable.name === 'CollectionObject' && (showBulkCarryRange || carryForwardAmount > 1) ? handleBulkCarryForward @@ -411,6 +420,7 @@ export function SaveButton({ ? copyButton( formsText.clone(), formsText.cloneDescription(), + false, async () => [await resource.clone(true)] ) : undefined} @@ -418,6 +428,7 @@ export function SaveButton({ copyButton( commonText.add(), formsText.addButtonDescription(), + false, async () => [new resource.specifyTable.Resource()] )} diff --git a/specifyweb/specify/tests/test_series_autonumber.py b/specifyweb/specify/tests/test_series_autonumber.py index 9c66ad3751b..01bb7641882 100644 --- a/specifyweb/specify/tests/test_series_autonumber.py +++ b/specifyweb/specify/tests/test_series_autonumber.py @@ -9,9 +9,6 @@ def test_series_autonumber_ranges(self): c = Client() c.force_login(self.specifyuser) - self.collection.catalognumformatname = "CatalogNumberNumeric" - self.collection.save() - # 000000005 to 000000007 response = c.post( f"/api/specify/series_autonumber_range/", @@ -20,6 +17,7 @@ def test_series_autonumber_ranges(self): 'rangeend': '7', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberNumeric', 'skipstartnumber': False, }), content_type="application/json", @@ -35,6 +33,7 @@ def test_series_autonumber_ranges(self): 'rangeend': '15', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberNumeric', 'skipstartnumber': True, }), content_type="application/json", @@ -50,6 +49,7 @@ def test_series_autonumber_ranges(self): 'rangeend': '0', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberNumeric', 'skipstartnumber': True }), content_type="application/json", @@ -64,6 +64,7 @@ def test_series_autonumber_ranges(self): 'rangeend': '10', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberNumeric', 'skipstartnumber': True, }), content_type="application/json", @@ -78,6 +79,7 @@ def test_series_autonumber_ranges(self): 'rangeend': '1000', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberNumeric', 'skipstartnumber': True, }), content_type="application/json", @@ -85,8 +87,6 @@ def test_series_autonumber_ranges(self): self.assertEqual(response.status_code, 400) # Test autonumbering with CatalogNumberAlphaNumByYear format - self.collection.catalognumformatname ="CatalogNumberAlphaNumByYear" - self.collection.save() response = c.post( f"/api/specify/series_autonumber_range/", data=json.dumps({ @@ -94,6 +94,7 @@ def test_series_autonumber_ranges(self): 'rangeend': '2023-000005', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberAlphaNumByYear', 'skipstartnumber': False, }), content_type="application/json", @@ -128,6 +129,7 @@ def test_series_autonumber_existing(self): 'rangeend': '20', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberNumeric', 'skipstartnumber': True, }), content_type="application/json", @@ -142,6 +144,7 @@ def test_series_autonumber_existing(self): 'rangeend': '10', 'tablename': 'collectionobject', 'fieldname': 'catalognumber', + 'formattername': 'CatalogNumberNumeric', 'skipstartnumber': True, }), content_type="application/json", diff --git a/specifyweb/specify/views.py b/specifyweb/specify/views.py index d34390f1fbb..b39d2aaad60 100644 --- a/specifyweb/specify/views.py +++ b/specifyweb/specify/views.py @@ -30,6 +30,7 @@ from specifyweb.specify.update_locality import localityupdate_parse_success, localityupdate_parse_error, parse_locality_set as _parse_locality_set, upload_locality_set as _upload_locality_set, create_localityupdate_recordset, update_locality_task, parse_locality_task, LocalityUpdateStatus from . import api, models as spmodels from .specify_jar import specify_jar, specify_jar_path +from .uiformatters import get_uiformatter_by_name logger = logging.getLogger(__name__) @@ -1502,8 +1503,6 @@ def catalog_number_from_parent(request: http.HttpRequest): return http.JsonResponse({'error': 'An internal server error occurred.'}, status=500) -from .uiformatters import get_uiformatter - @login_maybe_required @require_POST def series_autonumber_range(request: http.HttpRequest): @@ -1516,8 +1515,9 @@ def series_autonumber_range(request: http.HttpRequest): range_end = request_data.get('rangeend') table_name = request_data.get('tablename') field_name = request_data.get('fieldname') + formatter_name = request_data.get('formattername') - formatter = get_uiformatter(request.specify_collection, table_name, field_name) + formatter = get_uiformatter_by_name(request.specify_collection, request.specify_user, formatter_name) try: range_start_parsed = formatter.parse(range_start) From b93301158f87f541401205375dbe7e1cda4b0551 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Tue, 1 Jul 2025 13:12:29 -0500 Subject: [PATCH 19/33] Fix carry without record set --- .../frontend/js_src/lib/components/FormSliders/RecordSet.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx index cbb2fb82eda..7d64735e4d5 100644 --- a/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormSliders/RecordSet.tsx @@ -425,7 +425,6 @@ function RecordSet({ : undefined } onClone={(resources: RA>): void => { - console.log(resources); go(totalCount, 'new', resources[0]); if (resources.length > 1) { if (createRecordSetOnBulkCarryForward.includes( @@ -453,8 +452,8 @@ function RecordSet({ ); } else { // Don't create new record set - go(totalCount, resources.at(-1).id); handleAdd(resources.slice(1), false); + go(totalCount, resources.at(-1)?.id); } } }} From 4c2de15ac1f7471b65b573517355ab41585983d4 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Tue, 1 Jul 2025 15:26:40 -0500 Subject: [PATCH 20/33] Add series input to form --- .../lib/components/FieldFormatters/index.ts | 9 +- .../lib/components/FormFields/Field.tsx | 132 +++++++++++------- .../lib/components/FormFields/index.tsx | 2 + .../js_src/lib/components/FormParse/fields.ts | 2 + .../lib/components/Forms/BulkCarryForward.tsx | 12 ++ .../lib/components/Forms/ResourceView.tsx | 31 ++-- .../js_src/lib/components/Forms/Save.tsx | 38 ++++- 7 files changed, 161 insertions(+), 65 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts index 2e2310845cb..7cb7d2cfa7a 100644 --- a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts +++ b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts @@ -48,7 +48,8 @@ export const fetchContext = Promise.all([ formatter.title ?? formatter.name, fields, formatter.table, - formatter.field + formatter.field, + formatter.name ); } @@ -70,7 +71,8 @@ export class UiFormatter { public readonly fields: RA, public readonly table: SpecifyTable | undefined, // The field which this formatter is formatting - public readonly field: LiteralField | undefined + public readonly field: LiteralField | undefined, + public readonly name: string, ) {} /** @@ -297,7 +299,8 @@ export class CatalogNumberNumeric extends UiFormatter { }), ], tables.CollectionObject, - tables.CollectionObject?.getLiteralField('catalogNumber') + tables.CollectionObject?.getLiteralField('catalogNumber'), + 'CatalogNumberNumeric' ); } } diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 381dd5d8b40..72468d724d4 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -15,6 +15,7 @@ import { raise } from '../Errors/Crash'; import { fetchPathAsString } from '../Formatters/formatters'; import { collectionPreferences } from '../Preferences/collectionPreferences'; import { userPreferences } from '../Preferences/userPreferences'; +import { SeriesFormContext } from '../Forms/BulkCarryForward'; export function UiField({ field, @@ -24,6 +25,7 @@ export function UiField({ readonly name: string | undefined; readonly resource: SpecifyResource | undefined; readonly field: LiteralField | Relationship | undefined; + readonly isSeries: boolean | undefined; readonly parser?: Parser; }): JSX.Element { return field?.isRelationship === true ? ( @@ -95,12 +97,14 @@ function Field({ id, name, field, + isSeries, parser: defaultParser, }: { readonly resource: SpecifyResource | undefined; readonly id: string | undefined; readonly name: string | undefined; readonly field: LiteralField | undefined; + readonly isSeries: boolean | undefined; readonly parser?: Parser; }): JSX.Element { const { value, updateValue, validationRef, parser } = useResourceValue( @@ -208,55 +212,89 @@ function Field({ displayParentCatNumberPlaceHolder, ]); + const tableName = resource?.specifyTable.name; + const [enableCarryForward] = userPreferences.use( + 'form', + 'preferences', + 'enableCarryForward' + ); + const [enableBulkCarryForwardRange] = userPreferences.use( + 'form', + 'preferences', + 'enableBulkCarryForwardRange' + ); + + const placeholder = + displayPrimaryCatNumberPlaceHolder && + typeof primaryCatalogNumber === 'string' + ? primaryCatalogNumber + : displayParentCatNumberPlaceHolder && + typeof parentCatalogNumber === 'string' + ? parentCatalogNumber + : undefined; + + const { seriesEnd: seriesRangeEnd, setSeriesEnd: setSeriesRangeEnd, setUsingSeries } = React.useContext(SeriesFormContext); return ( - + updateValue(target.value) + } + onValueChange={(value): void => updateValue(value, false)} /* - * Disable "text-align: right" in non webkit browsers - * as they don't support spinner's arrow customization - */ - parser.type === 'number' && - rightAlignNumberFields && - globalThis.navigator.userAgent.toLowerCase().includes('webkit') - ? `text-right ${isReadOnly ? '' : 'pr-6'}` - : '' - } - id={id} - isReadOnly={isReadOnly} - required={'required' in validationAttributes && !isInSearchDialog} - tabIndex={isReadOnly ? -1 : undefined} - value={value?.toString() ?? ''} - onBlur={ - isReadOnly ? undefined : ({ target }): void => updateValue(target.value) + * Update data model value before onBlur, as onBlur fires after onSubmit + * if form is submitted using the ENTER key + */ + onChange={(event): void => { + const input = event.target as HTMLInputElement; + /* + * Don't show validation errors on value change for input fields until + * field is blurred, unless user tried to paste a date (see definition + * of Input.Generic) + */ + updateValue(input.value, event.type === 'paste'); + }} + /> + {isSeries && tableName && enableCarryForward.includes(tableName) && enableBulkCarryForwardRange.includes(tableName) && resource?.isNew() ? + { + const input = event.target as HTMLInputElement; + setSeriesRangeEnd(input.value); + setUsingSeries(true); + }} + placeholder={placeholder} + {...validationAttributes} + required={false} + /> : + undefined } - onValueChange={(value): void => updateValue(value, false)} - /* - * Update data model value before onBlur, as onBlur fires after onSubmit - * if form is submitted using the ENTER key - */ - onChange={(event): void => { - const input = event.target as HTMLInputElement; - /* - * Don't show validation errors on value change for input fields until - * field is blurred, unless user tried to paste a date (see definition - * of Input.Generic) - */ - updateValue(input.value, event.type === 'paste'); - }} - /> + ); } diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/index.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/index.tsx index 59a527a3e9a..6e9f8821d0b 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/index.tsx @@ -187,6 +187,7 @@ const fieldRenderers: { maxLength, minLength, whiteSpaceSensitive, + isSeries, }, }) { const parser = React.useMemo( @@ -218,6 +219,7 @@ const fieldRenderers: { name={name} parser={parser} resource={resource} + isSeries={isSeries} /> ); }, diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts b/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts index ebe8fa93f99..f2d7ee34a93 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/fields.ts @@ -82,6 +82,7 @@ export type FieldTypes = { readonly minLength: number | undefined; readonly maxLength: number | undefined; readonly whiteSpaceSensitive: boolean | undefined; + readonly isSeries: boolean | undefined; } >; readonly Plugin: State< @@ -218,6 +219,7 @@ const processFieldType: { minLength: f.parseInt(getProperty('minLength')), maxLength: f.parseInt(getProperty('maxLength')), whiteSpaceSensitive, + isSeries: getProperty('series')?.toLowerCase() === 'true', }; }, QueryComboBox({ getProperty, fields }) { diff --git a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx index edb61fc2c56..712d74cd94f 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx @@ -43,3 +43,15 @@ export function BulkCarryRangeBlockedDialog({ ); } + +export const SeriesFormContext = React.createContext<{ + seriesEnd: string; + setSeriesEnd: (v: string) => void; + usingSeries: boolean; + setUsingSeries: (v: boolean) => void; +}>({ + seriesEnd: '', + setSeriesEnd: () => {}, + usingSeries: false, + setUsingSeries: () => {}, +}); \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/Forms/ResourceView.tsx b/specifyweb/frontend/js_src/lib/components/Forms/ResourceView.tsx index a8ec2905fa7..a91fca6478d 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/ResourceView.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/ResourceView.tsx @@ -30,6 +30,7 @@ import { useResourceView } from './BaseResourceView'; import { DeleteButton } from './DeleteButton'; import { SaveButton } from './Save'; import { propsToFormMode } from './useViewDefinition'; +import { SeriesFormContext } from './BulkCarryForward'; /** * There is special behavior required when creating one of these resources, @@ -285,19 +286,29 @@ export function ResourceView({ ); + const [seriesRangeEnd, setSeriesRangeEnd] = React.useState(''); + const [usingSeries, setUsingSeries] = React.useState(false); + if (dialog === false) { const formattedChildren = ( <> - {formComponent} - {typeof deleteButton === 'object' || - typeof saveButtonElement === 'object' || - typeof extraButtons === 'object' ? ( - - {deleteButton} - {extraButtons ?? } - {saveButtonElement} - - ) : undefined} + + {formComponent} + {typeof deleteButton === 'object' || + typeof saveButtonElement === 'object' || + typeof extraButtons === 'object' ? ( + + {deleteButton} + {extraButtons ?? } + {saveButtonElement} + + ) : undefined} + ); diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index b6a11e49351..7849f53ade0 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -45,7 +45,7 @@ import { hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import { generateMappingPathPreview } from '../WbPlanView/mappingPreview'; import { FormContext } from './BaseResourceView'; -import { BulkCarryRangeBlockedDialog } from './BulkCarryForward'; +import { BulkCarryRangeBlockedDialog, SeriesFormContext } from './BulkCarryForward'; import { FORBID_ADDING, NO_CLONE } from './ResourceView'; export const saveFormUnloadProtect = formsText.unsavedFormUnloadProtect(); @@ -189,6 +189,22 @@ export function SaveButton({ : error(error_) ) .finally(() => { + if (usingSeriesForm && seriesRangeEndValue !== '') { + /* + * If using the in-form series input the record should not + * be saved before bulk carry forward. + */ + handleBulkCarryForward(true).then((resources) => { + if (handleAdd && resources !== undefined) { + handleAdd(resources); + } + }).then(() => { + unsetUnloadProtect(); + handleSaved?.(); + setIsSaving(false); + }) + return; + } unsetUnloadProtect(); handleSaved?.(); setIsSaving(false); @@ -246,6 +262,10 @@ export function SaveButton({ const [carryForwardAmount, setCarryForwardAmount] = React.useState(1); const [carryForwardRangeEnd, setCarryForwardRangeEnd] = React.useState(''); + const { seriesEnd: seriesRangeEndValue, usingSeries: usingSeriesForm } = React.useContext(SeriesFormContext); + React.useEffect(() => { + setCarryForwardRangeEnd(seriesRangeEndValue); + }, [seriesRangeEndValue]); const isCOGorCOJO = resource.specifyTable.name === 'CollectionObjectGroup' || @@ -266,7 +286,7 @@ export function SaveButton({ const [bulkCarryRangeInvalidNumbers, setBulkCarryRangeInvalidNumbers] = React.useState | undefined>(undefined); - const handleBulkCarryForward = async (): Promise< + const handleBulkCarryForward = async (saved=true): Promise< RA> | undefined > => { const numberFieldName = 'catalogNumber'; @@ -297,8 +317,8 @@ export function SaveButton({ rangeEnd: carryForwardRangeEnd, tableName: resource.specifyTable.name.toLowerCase(), fieldName: numberFieldName.toLowerCase(), - formatterName: formatter.title, - skipStartNumber: true, + formatterName: formatter.name, + skipStartNumber: saved, }), }) .then(({ data }) => data) @@ -330,14 +350,21 @@ export function SaveButton({ } ); + let recordsToBeSaved: RA>; const clones = await Promise.all(clonePromises); + if (saved === false) { + recordsToBeSaved = [resource, ...clones]; + } else { + recordsToBeSaved = clones; + } + const backendClones = await ajax>>( `/api/specify/bulk/${resource.specifyTable.name.toLowerCase()}/`, { method: 'POST', headers: { Accept: 'application/json' }, - body: clones, + body: recordsToBeSaved, } ).then(({ data }) => data.map((resource) => deserializeResource(serializeResource(resource))) @@ -373,6 +400,7 @@ export function SaveButton({ className="!w-fit" {...getValidationAttributes(parser)} placeholder={formatter.valueOrWild()} + isReadOnly={usingSeriesForm} value={carryForwardRangeEnd} width={numberField.datamodelDefinition.length} onValueChange={(value): void => From 8c697ad11e07639d186567b4f0526173080a660c Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 2 Jul 2025 07:48:36 -0500 Subject: [PATCH 21/33] Use field formatter name instead of title --- .../js_src/lib/components/FieldFormatters/index.ts | 9 ++++++--- specifyweb/frontend/js_src/lib/components/Forms/Save.tsx | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts index 2e2310845cb..cb222937acc 100644 --- a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts +++ b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts @@ -48,7 +48,8 @@ export const fetchContext = Promise.all([ formatter.title ?? formatter.name, fields, formatter.table, - formatter.field + formatter.field, + formatter.name ); } @@ -70,7 +71,8 @@ export class UiFormatter { public readonly fields: RA, public readonly table: SpecifyTable | undefined, // The field which this formatter is formatting - public readonly field: LiteralField | undefined + public readonly field: LiteralField | undefined, + public readonly name: string ) {} /** @@ -297,7 +299,8 @@ export class CatalogNumberNumeric extends UiFormatter { }), ], tables.CollectionObject, - tables.CollectionObject?.getLiteralField('catalogNumber') + tables.CollectionObject?.getLiteralField('catalogNumber'), + 'CatalogNumberNumeric' ); } } diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index b6a11e49351..7d40be4faba 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -297,7 +297,7 @@ export function SaveButton({ rangeEnd: carryForwardRangeEnd, tableName: resource.specifyTable.name.toLowerCase(), fieldName: numberFieldName.toLowerCase(), - formatterName: formatter.title, + formatterName: formatter.name, skipStartNumber: true, }), }) From 228cf97ce8575e22f235be7b773f2a05401efb38 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 2 Jul 2025 07:53:57 -0500 Subject: [PATCH 22/33] Update tests --- .../AttachmentsBulkImport/__tests__/utils.test.ts | 6 ++++-- .../js_src/lib/utils/parser/__tests__/definitions.test.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/__tests__/utils.test.ts b/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/__tests__/utils.test.ts index cd49c122ba5..6dbb39a6931 100644 --- a/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/__tests__/utils.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/__tests__/utils.test.ts @@ -56,7 +56,8 @@ const fileNameTestSpec: TestDefinition = { }), ], tables.CollectionObject, - tables.CollectionObject?.getLiteralField('catalogNumber') + tables.CollectionObject?.getLiteralField('catalogNumber'), + 'testNumeric' ), testCases: [ ['000.jpg', '000'], @@ -81,7 +82,8 @@ const fileNameTestSpec: TestDefinition = { }), ], tables.CollectionObject, - tables.CollectionObject?.getLiteralField('catalogNumber') + tables.CollectionObject?.getLiteralField('catalogNumber'), + 'testRegex' ), testCases: [ ['45265.jpg', '45265'], diff --git a/specifyweb/frontend/js_src/lib/utils/parser/__tests__/definitions.test.ts b/specifyweb/frontend/js_src/lib/utils/parser/__tests__/definitions.test.ts index 72e5974d38d..457b8d70dbc 100644 --- a/specifyweb/frontend/js_src/lib/utils/parser/__tests__/definitions.test.ts +++ b/specifyweb/frontend/js_src/lib/utils/parser/__tests__/definitions.test.ts @@ -74,7 +74,8 @@ const uiFormatter = new UiFormatter( localized('test'), formatterFields, tables.CollectionObject, - undefined + undefined, + 'test' ); const title = formsText.requiredFormat({ format: uiFormatter.pattern()! }); From 4887bb883cb7c0ed939cd443deb3fc0bee71bda1 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 2 Jul 2025 08:08:20 -0500 Subject: [PATCH 23/33] Update tests --- .../__snapshots__/index.test.ts.snap | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/FieldFormatters/__tests__/__snapshots__/index.test.ts.snap b/specifyweb/frontend/js_src/lib/components/FieldFormatters/__tests__/__snapshots__/index.test.ts.snap index 95c01ecf9bb..f9cc2770fc3 100644 --- a/specifyweb/frontend/js_src/lib/components/FieldFormatters/__tests__/__snapshots__/index.test.ts.snap +++ b/specifyweb/frontend/js_src/lib/components/FieldFormatters/__tests__/__snapshots__/index.test.ts.snap @@ -47,6 +47,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "AccessionNumber", "table": "[table Accession]", "title": "AccessionNumber", }, @@ -95,6 +96,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "AccessionNumberByYear", "table": "[table Accession]", "title": "AccessionNumberByYear", }, @@ -111,6 +113,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "AccessionStringFormatter", "table": "[table Accession]", "title": "AccessionStringFormatter", }, @@ -143,6 +146,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": false, + "name": "CatalogNumber", "table": "[table CollectionObject]", "title": "CatalogNumber", }, @@ -175,6 +179,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": false, + "name": "CatalogNumberAlphaNumByYear", "table": "[table CollectionObject]", "title": "CatalogNumberAlphaNumByYear", }, @@ -191,6 +196,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "CatalogNumberNumeric", "table": "[table CollectionObject]", "title": "Catalog Number Numeric", }, @@ -207,6 +213,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": false, + "name": "CatalogNumberNumericRegex", "table": "[table CollectionObject]", "title": "CatalogNumberNumericRegex", }, @@ -214,6 +221,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` "field": undefined, "fields": [], "isSystem": true, + "name": "Date", "table": undefined, "title": "Date", }, @@ -262,6 +270,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "DeaccessionNumber", "table": "[table Deaccession]", "title": "DeaccessionNumber", }, @@ -294,6 +303,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "GiftNumber", "table": "[table Gift]", "title": "GiftNumber", }, @@ -326,6 +336,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "InfoRequestNumber", "table": "[table InfoRequest]", "title": "InfoRequestNumber", }, @@ -342,6 +353,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "CatalogNumberNumeric", "table": "[table CollectionObject]", "title": "Catalog Number Numeric", }, @@ -374,6 +386,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "LoanNumber", "table": "[table Loan]", "title": "LoanNumber", }, @@ -390,6 +403,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "NumericBigDecimal", "table": undefined, "title": "NumericBigDecimal", }, @@ -406,6 +420,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "NumericByte", "table": undefined, "title": "NumericByte", }, @@ -422,6 +437,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "NumericDouble", "table": undefined, "title": "NumericDouble", }, @@ -438,6 +454,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "NumericFloat", "table": undefined, "title": "NumericFloat", }, @@ -454,6 +471,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "NumericInteger", "table": undefined, "title": "NumericInteger", }, @@ -470,6 +488,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "NumericLong", "table": undefined, "title": "NumericLong", }, @@ -486,6 +505,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` }, ], "isSystem": true, + "name": "NumericShort", "table": undefined, "title": "NumericShort", }, @@ -493,6 +513,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` "field": undefined, "fields": [], "isSystem": false, + "name": "PartialDate", "table": undefined, "title": "PartialDate", }, @@ -500,6 +521,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` "field": undefined, "fields": [], "isSystem": false, + "name": "PartialDateMonth", "table": undefined, "title": "PartialDateMonth", }, @@ -507,6 +529,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` "field": undefined, "fields": [], "isSystem": false, + "name": "PartialDateYear", "table": undefined, "title": "PartialDateYear", }, @@ -514,6 +537,7 @@ exports[`field formatters are fetched and parsed correctly 1`] = ` "field": undefined, "fields": [], "isSystem": true, + "name": "SearchDate", "table": undefined, "title": "SearchDate", }, From 191cff5b858c113534014f5abd7a7471d50b4b08 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 2 Jul 2025 08:25:41 -0500 Subject: [PATCH 24/33] Fix type --- specifyweb/frontend/js_src/lib/components/Forms/Save.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 7d40be4faba..353ad7be7a9 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -221,9 +221,7 @@ export function SaveButton({ label: LocalizedString, description: LocalizedString, disabled: boolean, - handleClick: () => - | Promise> | undefined> - | Promise>> + handleClick: () => Promise> | undefined> ): JSX.Element => ( Date: Wed, 2 Jul 2025 10:43:09 -0500 Subject: [PATCH 25/33] Update tests --- .../js_src/lib/components/FormParse/__tests__/cells.test.ts | 6 ++++++ .../FormParse/__tests__/postProcessFormDef.test.ts | 1 + .../js_src/lib/components/Forms/generateFormDefinition.ts | 1 + .../frontend/js_src/lib/components/Merging/CompareField.tsx | 1 + .../frontend/js_src/lib/components/WebLinks/index.tsx | 2 +- 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/cells.test.ts b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/cells.test.ts index a32c5297f2a..8d7f2d29b4d 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/cells.test.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/cells.test.ts @@ -117,6 +117,7 @@ describe('parseFormCell', () => { fieldDefinition: { defaultValue: undefined, isReadOnly: false, + isSeries: false, max: undefined, min: undefined, step: undefined, @@ -145,6 +146,7 @@ describe('parseFormCell', () => { fieldDefinition: { defaultValue: undefined, isReadOnly: false, + isSeries: false, max: undefined, min: undefined, step: undefined, @@ -170,6 +172,7 @@ describe('parseFormCell', () => { fieldDefinition: { defaultValue: undefined, isReadOnly: false, + isSeries: false, max: undefined, maxLength: undefined, min: undefined, @@ -203,6 +206,7 @@ describe('parseFormCell', () => { fieldDefinition: { defaultValue: undefined, isReadOnly: false, + isSeries: false, max: undefined, maxLength: undefined, min: undefined, @@ -253,6 +257,7 @@ describe('parseFormCell', () => { fieldDefinition: { defaultValue: 'A', isReadOnly: false, + isSeries: false, max: undefined, min: undefined, step: undefined, @@ -281,6 +286,7 @@ describe('parseFormCell', () => { fieldDefinition: { defaultValue: undefined, isReadOnly: false, + isSeries: false, max: undefined, min: undefined, step: undefined, diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/postProcessFormDef.test.ts b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/postProcessFormDef.test.ts index edb9259e971..835548295d8 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/postProcessFormDef.test.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/postProcessFormDef.test.ts @@ -94,6 +94,7 @@ const missingLabelTextField = ensure()({ fieldDefinition: { defaultValue: undefined, isReadOnly: false, + isSeries: false, min: undefined, max: undefined, step: undefined, diff --git a/specifyweb/frontend/js_src/lib/components/Forms/generateFormDefinition.ts b/specifyweb/frontend/js_src/lib/components/Forms/generateFormDefinition.ts index 3967409274a..b6b38669d5c 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/generateFormDefinition.ts +++ b/specifyweb/frontend/js_src/lib/components/Forms/generateFormDefinition.ts @@ -274,6 +274,7 @@ function getFieldDefinition( minLength: parser.minLength, maxLength: parser.maxLength, whiteSpaceSensitive: parser.whiteSpaceSensitive, + isSeries: false, }), }, }; diff --git a/specifyweb/frontend/js_src/lib/components/Merging/CompareField.tsx b/specifyweb/frontend/js_src/lib/components/Merging/CompareField.tsx index 5d09ed540bf..4b9e2f9812c 100644 --- a/specifyweb/frontend/js_src/lib/components/Merging/CompareField.tsx +++ b/specifyweb/frontend/js_src/lib/components/Merging/CompareField.tsx @@ -169,6 +169,7 @@ function fieldToDefinition( return { type: 'Text', defaultValue: undefined, + isSeries: false, min: undefined, max: undefined, minLength: undefined, diff --git a/specifyweb/frontend/js_src/lib/components/WebLinks/index.tsx b/specifyweb/frontend/js_src/lib/components/WebLinks/index.tsx index 89ecaaa0237..3ceb24c4f70 100644 --- a/specifyweb/frontend/js_src/lib/components/WebLinks/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WebLinks/index.tsx @@ -122,7 +122,7 @@ export function WebLinkField({ } > {formType === 'form' && typeof field === 'object' ? ( - + ) : undefined} {typeof definition === 'object' ? ( <> From c329f49605c1439dfc1b5b926186af7e4215302b Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 2 Jul 2025 10:58:33 -0500 Subject: [PATCH 26/33] Update tests --- .../lib/components/FormParse/__tests__/fields.test.ts | 7 +++++++ .../lib/components/FormParse/__tests__/index.test.ts | 2 ++ specifyweb/frontend/js_src/lib/tests/ajax/index.ts | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/fields.test.ts b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/fields.test.ts index ebfaaf420a4..47d12d08827 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/fields.test.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/fields.test.ts @@ -31,6 +31,7 @@ describe('parseFormField', () => { expect(parse('', {})).toEqual({ defaultValue: undefined, isReadOnly: false, + isSeries: false, max: undefined, min: undefined, step: undefined, @@ -46,6 +47,7 @@ describe('parseFormField', () => { ).toEqual({ defaultValue: 'a', isReadOnly: true, + isSeries: false, max: -10, min: 4, step: 3.2, @@ -61,6 +63,7 @@ describe('parseFormField', () => { ).toEqual({ defaultValue: 'abc', isReadOnly: true, + isSeries: false, max: -10, min: 4, step: 3.2, @@ -72,6 +75,7 @@ describe('parseFormField', () => { expect(parse('', {})).toEqual({ defaultValue: 'abc', isReadOnly: false, + isSeries: false, max: undefined, min: undefined, step: undefined, @@ -83,6 +87,7 @@ describe('parseFormField', () => { expect(parse('', {})).toEqual({ defaultValue: 'abc', isReadOnly: false, + isSeries: false, max: undefined, min: undefined, step: undefined, @@ -197,6 +202,7 @@ describe('parseFormField', () => { expect(parse('', {})).toEqual({ defaultValue: undefined, isReadOnly: false, + isSeries: false, type: 'Text', max: undefined, min: undefined, @@ -299,6 +305,7 @@ describe('parseFormField', () => { expect(parse('', {})).toEqual({ defaultValue: undefined, isReadOnly: false, + isSeries: false, type: 'Text', max: undefined, maxLength: undefined, diff --git a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/index.test.ts b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/index.test.ts index 77693cf8d0a..a4371e8c326 100644 --- a/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/index.test.ts +++ b/specifyweb/frontend/js_src/lib/components/FormParse/__tests__/index.test.ts @@ -105,6 +105,7 @@ const parsedFormView = { minLength: undefined, step: undefined, isReadOnly: false, + isSeries: false, defaultValue: undefined, whiteSpaceSensitive: false, }, @@ -579,6 +580,7 @@ test('parseRows', async () => { fieldDefinition: { defaultValue: undefined, isReadOnly: false, + isSeries: false, max: undefined, maxLength: undefined, min: undefined, diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts index 286814f5e8b..6c2ed143358 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts +++ b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts @@ -117,7 +117,7 @@ export async function ajaxMock( * Windows. */ const [splitUrl, queryString = ''] = url.split('?'); - const parsedPath = path.parse(`${basePath}/lib/tests/ajax/static${splitUrl}`); + const parsedPath = path.parse(`C:/Users/a114s239/Projects/specify7/specifyweb/frontend/js_src/${basePath}/lib/tests/ajax/static${splitUrl}`); const directoryName = queryString === '' ? parsedPath.dir From 23a2463b91a67244d693826c3b106650484f6f14 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Wed, 2 Jul 2025 11:02:43 -0500 Subject: [PATCH 27/33] Undo commit --- specifyweb/frontend/js_src/lib/tests/ajax/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts index 6c2ed143358..286814f5e8b 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts +++ b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts @@ -117,7 +117,7 @@ export async function ajaxMock( * Windows. */ const [splitUrl, queryString = ''] = url.split('?'); - const parsedPath = path.parse(`C:/Users/a114s239/Projects/specify7/specifyweb/frontend/js_src/${basePath}/lib/tests/ajax/static${splitUrl}`); + const parsedPath = path.parse(`${basePath}/lib/tests/ajax/static${splitUrl}`); const directoryName = queryString === '' ? parsedPath.dir From 9a9b09230606c62713b39993e445697defecc4b0 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Thu, 21 May 2026 12:27:15 -0500 Subject: [PATCH 28/33] Fix merge errors --- .../lib/components/FieldFormatters/index.ts | 4 ---- .../js_src/lib/components/Forms/Save.tsx | 4 ++-- .../frontend/js_src/lib/localization/forms.ts | 19 ------------------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts index 659029d94b4..97634d21b1e 100644 --- a/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts +++ b/specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts @@ -134,10 +134,6 @@ export class UiFormatter { return this.parts.some((part) => part.autoIncrement); } - public canAutoIncrement(): boolean { - return this.fields.some((field) => field.autoIncrement); - } - public format(value: string): LocalizedString | undefined { const parsed = this.parse(value); return parsed === undefined ? undefined : this.canonicalize(parsed); diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 5a65cda5aff..1a60d4d9d10 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -34,7 +34,7 @@ import { hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import { generateMappingPathPreview } from '../WbPlanView/mappingPreview'; import { FormContext } from './BaseResourceView'; -import { useBulkCarryForward, SeriesFormContext, handleBulkCarryForward } from './BulkCarryForward'; +import { useBulkCarryForward, SeriesFormContext } from './BulkCarryForward'; import { FORBID_ADDING, NO_CLONE } from './ResourceView'; export const saveFormUnloadProtect = formsText.unsavedFormUnloadProtect(); @@ -249,7 +249,7 @@ export function SaveButton({ ); - const { seriesEnd: seriesRangeEndValue, usingSeries: usingSeriesForm } = React.useContext(SeriesFormContext); + // const { seriesEnd: seriesRangeEndValue, usingSeries: usingSeriesForm } = React.useContext(SeriesFormContext); const { BulkCarryForward, dialogs: BulkCarryForwardDialogs, diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 45003cb03eb..d5e281fecbc 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -1121,25 +1121,6 @@ export const formsText = createDictionary({ 'uk-ua': 'Створення набору записів для групового перенесення', 'hr-hr': 'Stvori skup zapisa pri skupnom prijenosu naprijed', }, - bulkCarryForwardRangeEnabled: { - 'en-us': 'Show Bulk Carry Forward range', - }, - bulkCarryForwardRangeErrorDescription: { - 'en-us': - 'Cannot carry forward record through the specified {field:string} range.', - }, - bulkCarryForwardRangeExistingRecords: { - 'en-us': 'The following numbers for {field:string} are already being used:', - }, - bulkCarryForwardRangeStart: { - 'en-us': 'Carry Forward Range Start', - }, - bulkCarryForwardRangeEnd: { - 'en-us': 'Carry Forward Range End', - }, - createRecordSetOnBulkCarryForward: { - 'en-us': 'Create record set on Bulk Carry Forward', - }, cloneButtonEnabled: { 'en-us': 'Show Clone button', 'ru-ru': 'Показать кнопку «Клон»', From 1e40149e1b84f6ccbd280144be4ca4956a05b730 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Thu, 21 May 2026 13:15:44 -0500 Subject: [PATCH 29/33] Fix merge conflict --- .../lib/components/FormFields/Field.tsx | 2 +- .../lib/components/Forms/BulkCarryForward.tsx | 13 +++++-- .../js_src/lib/components/Forms/Save.tsx | 34 +++++++++---------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 0d4b9b14e7e..e4b5bc516ea 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -279,7 +279,7 @@ function Field({ setSeriesRangeEnd(input.value); setUsingSeries(true); }} - placeholder={placeholder} + placeholder={customPlaceholder} {...validationAttributes} required={false} /> : diff --git a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx index c9441afee52..b29023f19e1 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx @@ -42,7 +42,7 @@ export function useBulkCarryForward({ }): { readonly BulkCarryForward: JSX.Element | null; readonly handleBulkCarryForward: - | (() => Promise> | undefined>) + | ((saved: boolean) => Promise> | undefined>) | undefined; readonly dialogs: JSX.Element | null; } { @@ -97,7 +97,7 @@ function useBulkCarryForwardRange( const handleBulkCarryForward = typeof formatter === 'object' - ? async (): Promise> | undefined> => { + ? async (saved=true): Promise> | undefined> => { const carryForwardRangeStart = resource.get(field.name); if ( carryForwardRangeStart === null || @@ -146,6 +146,7 @@ function useBulkCarryForwardRange( return undefined; } + let recordsToBeSaved: RA>; const clones = await Promise.all( response.map(async (value) => { const clonedResource = await resource.clone(false, true); @@ -154,12 +155,18 @@ function useBulkCarryForwardRange( }) ); + if (saved === false) { + recordsToBeSaved = [resource, ...clones]; + } else { + recordsToBeSaved = clones; + } + const backendClones = await ajax>>( `/bulk_copy/bulk/${resource.specifyTable.name.toLowerCase()}/`, { method: 'POST', headers: { Accept: 'application/json' }, - body: clones, + body: recordsToBeSaved, } ).then(({ data }) => data.map((resource) => diff --git a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx index 1a60d4d9d10..1cb6c06fa35 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/Save.tsx @@ -180,22 +180,22 @@ export function SaveButton({ .then(handleSaved) .finally(() => { // TODO: Update this code and uncomment - // if (usingSeriesForm && seriesRangeEndValue !== '') { - // /* - // * If using the in-form series input the record should not - // * be saved before bulk carry forward. - // */ - // handleBulkCarryForward(true).then((resources) => { - // if (handleAdd && resources !== undefined) { - // handleAdd(resources); - // } - // }).then(() => { - // unsetUnloadProtect(); - // handleSaved?.(); - // setIsSaving(false); - // }) - // return; - // } + if (usingSeriesForm && seriesRangeEndValue !== '' && handleBulkCarryForward !== undefined) { + /* + * If using the in-form series input the record should not + * be saved before bulk carry forward. + */ + handleBulkCarryForward(true).then((resources) => { + if (handleAdd && resources !== undefined) { + handleAdd(resources); + } + }).then(() => { + unsetUnloadProtect(); + handleSaved?.(); + setIsSaving(false); + }) + return; + } unsetUnloadProtect(); setIsSaving(false); }); @@ -249,7 +249,7 @@ export function SaveButton({ ); - // const { seriesEnd: seriesRangeEndValue, usingSeries: usingSeriesForm } = React.useContext(SeriesFormContext); + const { seriesEnd: seriesRangeEndValue, usingSeries: usingSeriesForm } = React.useContext(SeriesFormContext); const { BulkCarryForward, dialogs: BulkCarryForwardDialogs, From ff7910c20fa72e66d0b58c0d6f12d2adb9beb06f Mon Sep 17 00:00:00 2001 From: alesan99 Date: Thu, 21 May 2026 13:18:45 -0500 Subject: [PATCH 30/33] Potential fix for pull request finding 'CodeQL / Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../frontend/js_src/lib/components/Forms/BulkCarryForward.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx index b29023f19e1..34a1638a779 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx @@ -85,7 +85,7 @@ function useBulkCarryForwardRange( const [carryForwardRangeEnd, setCarryForwardRangeEnd] = React.useState(''); - const { seriesEnd: seriesRangeEndValue, usingSeries: usingSeriesForm } = React.useContext(SeriesFormContext); + const { seriesEnd: seriesRangeEndValue } = React.useContext(SeriesFormContext); React.useEffect(() => { setCarryForwardRangeEnd(seriesRangeEndValue); }, [seriesRangeEndValue]); From 0db052f6b076e50ada461bf6692a9db5f81d2f9b Mon Sep 17 00:00:00 2001 From: alesan99 Date: Thu, 21 May 2026 13:32:31 -0500 Subject: [PATCH 31/33] Fix function signature --- .../frontend/js_src/lib/components/Forms/BulkCarryForward.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx index 34a1638a779..2cde8038ef4 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx @@ -42,7 +42,7 @@ export function useBulkCarryForward({ }): { readonly BulkCarryForward: JSX.Element | null; readonly handleBulkCarryForward: - | ((saved: boolean) => Promise> | undefined>) + | ((saved?: boolean) => Promise> | undefined>) | undefined; readonly dialogs: JSX.Element | null; } { From 1014063fd48f6be5bd25cb1c85045367d7f03707 Mon Sep 17 00:00:00 2001 From: alesan99 Date: Thu, 21 May 2026 13:35:25 -0500 Subject: [PATCH 32/33] Change handleBulkCarryForward default behavior --- .../frontend/js_src/lib/components/Forms/BulkCarryForward.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx index 2cde8038ef4..8edabb18777 100644 --- a/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx +++ b/specifyweb/frontend/js_src/lib/components/Forms/BulkCarryForward.tsx @@ -97,7 +97,7 @@ function useBulkCarryForwardRange( const handleBulkCarryForward = typeof formatter === 'object' - ? async (saved=true): Promise> | undefined> => { + ? async (saved=false): Promise> | undefined> => { const carryForwardRangeStart = resource.get(field.name); if ( carryForwardRangeStart === null || From 7b1a565211d3b00af659a08fca837d8315a5af6e Mon Sep 17 00:00:00 2001 From: alesan99 Date: Thu, 21 May 2026 14:34:46 -0500 Subject: [PATCH 33/33] Refactor seriesInput in field.tsx --- .../lib/components/FormFields/Field.tsx | 142 ++++++++++++------ 1 file changed, 98 insertions(+), 44 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index e4b5bc516ea..0c013c1d69c 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -13,9 +13,9 @@ import { resourceOn } from '../DataModel/resource'; import type { LiteralField, Relationship } from '../DataModel/specifyField'; import { raise } from '../Errors/Crash'; import { fetchPathAsString } from '../Formatters/formatters'; +import { SeriesFormContext } from '../Forms/BulkCarryForward'; import { collectionPreferences } from '../Preferences/collectionPreferences'; import { userPreferences } from '../Preferences/userPreferences'; -import { SeriesFormContext } from '../Forms/BulkCarryForward'; export function UiField({ field, @@ -30,6 +30,8 @@ export function UiField({ }): JSX.Element { return field?.isRelationship === true ? ( + ) : props.isSeries === true ? ( + ) : ( ); @@ -92,20 +94,26 @@ function RelationshipField({ ); } +type FieldSeriesInput = (props: { + readonly name: string | undefined; + readonly placeholder: string | undefined; + readonly validationAttributes: ReturnType; +}) => React.ReactNode; + function Field({ resource, id, name, field, - isSeries, parser: defaultParser, + seriesInput, }: { readonly resource: SpecifyResource | undefined; readonly id: string | undefined; readonly name: string | undefined; readonly field: LiteralField | undefined; - readonly isSeries: boolean | undefined; readonly parser?: Parser; + readonly seriesInput?: FieldSeriesInput; }): JSX.Element { const { value, updateValue, validationRef, parser } = useResourceValue( resource, @@ -208,18 +216,6 @@ function Field({ displayParentCatNumberPlaceHolder, ]); - const tableName = resource?.specifyTable.name; - const [enableCarryForward] = userPreferences.use( - 'form', - 'preferences', - 'enableCarryForward' - ); - const [enableBulkCarryForwardRange] = userPreferences.use( - 'form', - 'preferences', - 'enableBulkCarryForwardRange' - ); - const customPlaceholder = displayPrimaryCatNumberPlaceHolder && typeof primaryCatalogNumber === 'string' @@ -228,12 +224,10 @@ function Field({ typeof parentCatalogNumber === 'string' ? parentCatalogNumber : undefined; - + const { placeholder: parserPlaceholder, ...restValidationAttributes } = validationAttributes; - const { seriesEnd: seriesRangeEnd, setSeriesEnd: setSeriesRangeEnd, setUsingSeries } = React.useContext(SeriesFormContext); - return ( <> updateValue(target.value) + isReadOnly + ? undefined + : ({ target }): void => updateValue(target.value) } onValueChange={(value): void => updateValue(value, false)} /* - * Update data model value before onBlur, as onBlur fires after onSubmit - * if form is submitted using the ENTER key - */ + * Update data model value before onBlur, as onBlur fires after onSubmit + * if form is submitted using the ENTER key + */ onChange={(event): void => { const input = event.target as HTMLInputElement; /* - * Don't show validation errors on value change for input fields until - * field is blurred, unless user tried to paste a date (see definition - * of Input.Generic) - */ + * Don't show validation errors on value change for input fields until + * field is blurred, unless user tried to paste a date (see definition + * of Input.Generic) + */ updateValue(input.value, event.type === 'paste'); }} /> - {isSeries && tableName && enableCarryForward.includes(tableName) && enableBulkCarryForwardRange.includes(tableName) && resource?.isNew() ? - { - const input = event.target as HTMLInputElement; - setSeriesRangeEnd(input.value); - setUsingSeries(true); - }} - placeholder={customPlaceholder} - {...validationAttributes} - required={false} - /> : - undefined - } + {seriesInput?.({ + name, + placeholder: customPlaceholder, + validationAttributes, + })} ); } +function SeriesField({ + resource, + field, + id, + name, + isSeries, + parser, +}: { + readonly id: string | undefined; + readonly name: string | undefined; + readonly resource: SpecifyResource | undefined; + readonly field: LiteralField | undefined; + readonly isSeries: boolean | undefined; + readonly parser?: Parser; +}): JSX.Element { + const { + seriesEnd: seriesRangeEnd, + setSeriesEnd: setSeriesRangeEnd, + setUsingSeries, + } = React.useContext(SeriesFormContext); + const tableName = resource?.specifyTable.name; + const [enableCarryForward] = userPreferences.use( + 'form', + 'preferences', + 'enableCarryForward' + ); + const [enableBulkCarryForwardRange] = userPreferences.use( + 'form', + 'preferences', + 'enableBulkCarryForwardRange' + ); + const showSeriesInput = + isSeries === true && + tableName !== undefined && + enableCarryForward.includes(tableName) && + enableBulkCarryForwardRange.includes(tableName) && + resource?.isNew() === true; + const handleSeriesRangeEndChange = ( + event: React.ChangeEvent + ): void => { + const input = event.target as HTMLInputElement; + setSeriesRangeEnd(input.value); + setUsingSeries(true); + }; + + return ( + + showSeriesInput ? ( + + ) : null + } + /> + ); +} + export function useRightAlignClassName( type: Parser['type'], isReadOnly: boolean