From b11ba07aabde6d3d321759500ee9bdaf2697b671 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:28:39 +0000 Subject: [PATCH 01/14] Initial plan From 0bc502e0b5ef1b70c52afa5696032c29981aa6fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:41:01 +0000 Subject: [PATCH 02/14] feat(i18n): add objectManager and fieldDesigner keys to all 9 non-English locales Add translated objectManager and fieldDesigner sub-sections to the appDesigner section in zh, ja, ko, de, fr, es, pt, ru, and ar locale files, matching the keys added to en.ts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/i18n/src/locales/ar.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/de.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/es.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/fr.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/ja.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/ko.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/pt.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/ru.ts | 52 +++++++++++++++++++++++++++++++++ packages/i18n/src/locales/zh.ts | 52 +++++++++++++++++++++++++++++++++ 9 files changed, 468 insertions(+) diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 8a902bba..2f230c80 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -472,6 +472,58 @@ const ar = { modeLight: 'فاتح', modeDark: 'داكن', mobilePreview: 'معاينة الجوال', + objectManager: { + title: 'مدير الكائنات', + addObject: 'كائن جديد', + searchPlaceholder: 'بحث عن كائنات…', + noObjects: 'لم يتم العثور على كائنات.', + objectName: 'اسم API', + objectLabel: 'التسمية', + pluralLabel: 'التسمية الجمعية', + icon: 'الأيقونة', + selectIcon: 'اختيار أيقونة…', + group: 'المجموعة', + noGroup: 'بدون مجموعة', + sortOrder: 'ترتيب الفرز', + enabled: 'مفعّل', + relationships: 'العلاقات', + systemBadge: 'نظام', + fieldCount: '{{count}} حقول', + ungrouped: 'غير مصنف', + deleteConfirmTitle: 'حذف الكائن؟', + deleteConfirmMessage: 'سيتم حذف الكائن وجميع حقوله نهائيًا. لا يمكن التراجع عن هذا الإجراء.', + }, + fieldDesigner: { + title: 'مصمم الحقول', + addField: 'حقل جديد', + searchPlaceholder: 'بحث عن حقول…', + allTypes: 'كل الأنواع', + noFields: 'لم يتم العثور على حقول.', + fieldName: 'اسم API', + fieldLabel: 'التسمية', + fieldType: 'النوع', + fieldGroup: 'المجموعة', + description: 'الوصف', + required: 'مطلوب', + unique: 'فريد', + readOnly: 'للقراءة فقط', + hidden: 'مخفي', + indexed: 'مفهرس', + externalId: 'معرف خارجي', + trackHistory: 'تتبع السجل', + defaultValue: 'القيمة الافتراضية', + placeholder: 'نص توضيحي', + referenceTo: 'مرجع إلى', + formula: 'صيغة', + options: 'خيارات', + addOption: 'إضافة خيار', + validationRules: 'قواعد التحقق', + addRule: 'إضافة قاعدة', + systemBadge: 'نظام', + ungrouped: 'عام', + deleteConfirmTitle: 'حذف الحقل؟', + deleteConfirmMessage: 'سيتم حذف الحقل نهائيًا. ستفقد البيانات الموجودة في هذا الحقل.', + }, }, console: { title: 'وحدة تحكم ObjectStack', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 80d2ae56..b57f222e 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -480,6 +480,58 @@ const de = { modeLight: 'Hell', modeDark: 'Dunkel', mobilePreview: 'Mobile Vorschau', + objectManager: { + title: 'Objekt-Manager', + addObject: 'Neues Objekt', + searchPlaceholder: 'Objekte suchen…', + noObjects: 'Keine Objekte gefunden.', + objectName: 'API-Name', + objectLabel: 'Bezeichnung', + pluralLabel: 'Pluralbezeichnung', + icon: 'Symbol', + selectIcon: 'Symbol wählen…', + group: 'Gruppe', + noGroup: 'Keine Gruppe', + sortOrder: 'Sortierung', + enabled: 'Aktiviert', + relationships: 'Beziehungen', + systemBadge: 'System', + fieldCount: '{{count}} Felder', + ungrouped: 'Nicht gruppiert', + deleteConfirmTitle: 'Objekt löschen?', + deleteConfirmMessage: 'Das Objekt und alle zugehörigen Felder werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.', + }, + fieldDesigner: { + title: 'Feld-Designer', + addField: 'Neues Feld', + searchPlaceholder: 'Felder suchen…', + allTypes: 'Alle Typen', + noFields: 'Keine Felder gefunden.', + fieldName: 'API-Name', + fieldLabel: 'Bezeichnung', + fieldType: 'Typ', + fieldGroup: 'Gruppe', + description: 'Beschreibung', + required: 'Erforderlich', + unique: 'Eindeutig', + readOnly: 'Schreibgeschützt', + hidden: 'Ausgeblendet', + indexed: 'Indiziert', + externalId: 'Externe ID', + trackHistory: 'Verlauf verfolgen', + defaultValue: 'Standardwert', + placeholder: 'Platzhalter', + referenceTo: 'Verweis auf', + formula: 'Formel', + options: 'Optionen', + addOption: 'Option hinzufügen', + validationRules: 'Validierungsregeln', + addRule: 'Regel hinzufügen', + systemBadge: 'System', + ungrouped: 'Allgemein', + deleteConfirmTitle: 'Feld löschen?', + deleteConfirmMessage: 'Das Feld wird dauerhaft gelöscht. Vorhandene Daten in diesem Feld gehen verloren.', + }, }, console: { title: 'ObjectStack Konsole', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 349c360f..ca16d8b9 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -471,6 +471,58 @@ const es = { modeLight: 'Claro', modeDark: 'Oscuro', mobilePreview: 'Vista previa móvil', + objectManager: { + title: 'Gestor de objetos', + addObject: 'Nuevo objeto', + searchPlaceholder: 'Buscar objetos…', + noObjects: 'No se encontraron objetos.', + objectName: 'Nombre API', + objectLabel: 'Etiqueta', + pluralLabel: 'Etiqueta plural', + icon: 'Icono', + selectIcon: 'Seleccionar icono…', + group: 'Grupo', + noGroup: 'Sin grupo', + sortOrder: 'Orden', + enabled: 'Habilitado', + relationships: 'Relaciones', + systemBadge: 'Sistema', + fieldCount: '{{count}} campos', + ungrouped: 'Sin agrupar', + deleteConfirmTitle: '¿Eliminar objeto?', + deleteConfirmMessage: 'El objeto y todos sus campos se eliminarán permanentemente. Esta acción no se puede deshacer.', + }, + fieldDesigner: { + title: 'Diseñador de campos', + addField: 'Nuevo campo', + searchPlaceholder: 'Buscar campos…', + allTypes: 'Todos los tipos', + noFields: 'No se encontraron campos.', + fieldName: 'Nombre API', + fieldLabel: 'Etiqueta', + fieldType: 'Tipo', + fieldGroup: 'Grupo', + description: 'Descripción', + required: 'Obligatorio', + unique: 'Único', + readOnly: 'Solo lectura', + hidden: 'Oculto', + indexed: 'Indexado', + externalId: 'ID externo', + trackHistory: 'Rastrear historial', + defaultValue: 'Valor predeterminado', + placeholder: 'Texto de ejemplo', + referenceTo: 'Referencia a', + formula: 'Fórmula', + options: 'Opciones', + addOption: 'Agregar opción', + validationRules: 'Reglas de validación', + addRule: 'Agregar regla', + systemBadge: 'Sistema', + ungrouped: 'General', + deleteConfirmTitle: '¿Eliminar campo?', + deleteConfirmMessage: 'El campo se eliminará permanentemente. Los datos existentes en este campo se perderán.', + }, }, console: { title: 'Consola ObjectStack', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index b26ab15b..c2b86b97 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -480,6 +480,58 @@ const fr = { modeLight: 'Clair', modeDark: 'Sombre', mobilePreview: 'Aperçu mobile', + objectManager: { + title: 'Gestionnaire d\'objets', + addObject: 'Nouvel objet', + searchPlaceholder: 'Rechercher des objets…', + noObjects: 'Aucun objet trouvé.', + objectName: 'Nom API', + objectLabel: 'Libellé', + pluralLabel: 'Libellé pluriel', + icon: 'Icône', + selectIcon: 'Sélectionner une icône…', + group: 'Groupe', + noGroup: 'Aucun groupe', + sortOrder: 'Ordre de tri', + enabled: 'Activé', + relationships: 'Relations', + systemBadge: 'Système', + fieldCount: '{{count}} champs', + ungrouped: 'Non groupé', + deleteConfirmTitle: 'Supprimer l\'objet ?', + deleteConfirmMessage: 'L\'objet et tous ses champs seront définitivement supprimés. Cette action est irréversible.', + }, + fieldDesigner: { + title: 'Concepteur de champs', + addField: 'Nouveau champ', + searchPlaceholder: 'Rechercher des champs…', + allTypes: 'Tous les types', + noFields: 'Aucun champ trouvé.', + fieldName: 'Nom API', + fieldLabel: 'Libellé', + fieldType: 'Type', + fieldGroup: 'Groupe', + description: 'Description', + required: 'Obligatoire', + unique: 'Unique', + readOnly: 'Lecture seule', + hidden: 'Masqué', + indexed: 'Indexé', + externalId: 'ID externe', + trackHistory: 'Suivi de l\'historique', + defaultValue: 'Valeur par défaut', + placeholder: 'Texte indicatif', + referenceTo: 'Référence vers', + formula: 'Formule', + options: 'Options', + addOption: 'Ajouter une option', + validationRules: 'Règles de validation', + addRule: 'Ajouter une règle', + systemBadge: 'Système', + ungrouped: 'Général', + deleteConfirmTitle: 'Supprimer le champ ?', + deleteConfirmMessage: 'Le champ sera définitivement supprimé. Les données existantes seront perdues.', + }, }, console: { title: 'Console ObjectStack', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index eeaf1c96..30177953 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -482,6 +482,58 @@ const ja = { modeLight: 'ライト', modeDark: 'ダーク', mobilePreview: 'モバイルプレビュー', + objectManager: { + title: 'オブジェクトマネージャー', + addObject: '新規オブジェクト', + searchPlaceholder: 'オブジェクトを検索…', + noObjects: 'オブジェクトが見つかりません。', + objectName: 'API名', + objectLabel: 'ラベル', + pluralLabel: '複数形ラベル', + icon: 'アイコン', + selectIcon: 'アイコンを選択…', + group: 'グループ', + noGroup: 'グループなし', + sortOrder: '並び順', + enabled: '有効', + relationships: 'リレーション', + systemBadge: 'システム', + fieldCount: '{{count}} フィールド', + ungrouped: '未分類', + deleteConfirmTitle: 'オブジェクトを削除しますか?', + deleteConfirmMessage: 'このオブジェクトとそのすべてのフィールドが完全に削除されます。この操作は元に戻せません。', + }, + fieldDesigner: { + title: 'フィールドデザイナー', + addField: '新規フィールド', + searchPlaceholder: 'フィールドを検索…', + allTypes: 'すべてのタイプ', + noFields: 'フィールドが見つかりません。', + fieldName: 'API名', + fieldLabel: 'ラベル', + fieldType: 'タイプ', + fieldGroup: 'グループ', + description: '説明', + required: '必須', + unique: 'ユニーク', + readOnly: '読み取り専用', + hidden: '非表示', + indexed: 'インデックス', + externalId: '外部ID', + trackHistory: '履歴追跡', + defaultValue: 'デフォルト値', + placeholder: 'プレースホルダー', + referenceTo: '参照先', + formula: '数式', + options: 'オプション', + addOption: 'オプション追加', + validationRules: '入力規則', + addRule: 'ルール追加', + systemBadge: 'システム', + ungrouped: '一般', + deleteConfirmTitle: 'フィールドを削除しますか?', + deleteConfirmMessage: 'このフィールドは完全に削除されます。既存のデータは失われます。', + }, }, console: { title: 'ObjectStack コンソール', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 6560a68d..b44ffa50 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -471,6 +471,58 @@ const ko = { modeLight: '라이트', modeDark: '다크', mobilePreview: '모바일 미리보기', + objectManager: { + title: '개체 관리자', + addObject: '새 개체', + searchPlaceholder: '개체 검색…', + noObjects: '개체를 찾을 수 없습니다.', + objectName: 'API 이름', + objectLabel: '레이블', + pluralLabel: '복수 레이블', + icon: '아이콘', + selectIcon: '아이콘 선택…', + group: '그룹', + noGroup: '그룹 없음', + sortOrder: '정렬 순서', + enabled: '활성화', + relationships: '관계', + systemBadge: '시스템', + fieldCount: '{{count}} 필드', + ungrouped: '미분류', + deleteConfirmTitle: '개체를 삭제하시겠습니까?', + deleteConfirmMessage: '이 개체와 모든 필드가 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다.', + }, + fieldDesigner: { + title: '필드 디자이너', + addField: '새 필드', + searchPlaceholder: '필드 검색…', + allTypes: '모든 유형', + noFields: '필드를 찾을 수 없습니다.', + fieldName: 'API 이름', + fieldLabel: '레이블', + fieldType: '유형', + fieldGroup: '그룹', + description: '설명', + required: '필수', + unique: '고유', + readOnly: '읽기 전용', + hidden: '숨김', + indexed: '인덱스', + externalId: '외부 ID', + trackHistory: '이력 추적', + defaultValue: '기본값', + placeholder: '플레이스홀더', + referenceTo: '참조 대상', + formula: '수식', + options: '옵션', + addOption: '옵션 추가', + validationRules: '유효성 규칙', + addRule: '규칙 추가', + systemBadge: '시스템', + ungrouped: '일반', + deleteConfirmTitle: '필드를 삭제하시겠습니까?', + deleteConfirmMessage: '이 필드가 영구적으로 삭제됩니다. 기존 데이터가 손실됩니다.', + }, }, console: { title: 'ObjectStack 콘솔', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 3a1878c8..12c18da5 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -471,6 +471,58 @@ const pt = { modeLight: 'Claro', modeDark: 'Escuro', mobilePreview: 'Pré-visualização móvel', + objectManager: { + title: 'Gerenciador de Objetos', + addObject: 'Novo Objeto', + searchPlaceholder: 'Pesquisar objetos…', + noObjects: 'Nenhum objeto encontrado.', + objectName: 'Nome API', + objectLabel: 'Rótulo', + pluralLabel: 'Rótulo plural', + icon: 'Ícone', + selectIcon: 'Selecionar ícone…', + group: 'Grupo', + noGroup: 'Sem grupo', + sortOrder: 'Ordem', + enabled: 'Habilitado', + relationships: 'Relacionamentos', + systemBadge: 'Sistema', + fieldCount: '{{count}} campos', + ungrouped: 'Sem grupo', + deleteConfirmTitle: 'Excluir objeto?', + deleteConfirmMessage: 'O objeto e todos os seus campos serão excluídos permanentemente. Esta ação não pode ser desfeita.', + }, + fieldDesigner: { + title: 'Designer de Campos', + addField: 'Novo Campo', + searchPlaceholder: 'Pesquisar campos…', + allTypes: 'Todos os tipos', + noFields: 'Nenhum campo encontrado.', + fieldName: 'Nome API', + fieldLabel: 'Rótulo', + fieldType: 'Tipo', + fieldGroup: 'Grupo', + description: 'Descrição', + required: 'Obrigatório', + unique: 'Único', + readOnly: 'Somente leitura', + hidden: 'Oculto', + indexed: 'Indexado', + externalId: 'ID Externo', + trackHistory: 'Rastrear histórico', + defaultValue: 'Valor padrão', + placeholder: 'Texto de exemplo', + referenceTo: 'Referência para', + formula: 'Fórmula', + options: 'Opções', + addOption: 'Adicionar opção', + validationRules: 'Regras de validação', + addRule: 'Adicionar regra', + systemBadge: 'Sistema', + ungrouped: 'Geral', + deleteConfirmTitle: 'Excluir campo?', + deleteConfirmMessage: 'O campo será excluído permanentemente. Os dados existentes neste campo serão perdidos.', + }, }, console: { title: 'Console ObjectStack', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 8be9298a..fb878208 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -491,6 +491,58 @@ const ru = { modeLight: 'Светлая', modeDark: 'Тёмная', mobilePreview: 'Мобильный предпросмотр', + objectManager: { + title: 'Менеджер объектов', + addObject: 'Новый объект', + searchPlaceholder: 'Поиск объектов…', + noObjects: 'Объекты не найдены.', + objectName: 'Имя API', + objectLabel: 'Метка', + pluralLabel: 'Метка (мн. число)', + icon: 'Иконка', + selectIcon: 'Выбрать иконку…', + group: 'Группа', + noGroup: 'Без группы', + sortOrder: 'Порядок сортировки', + enabled: 'Включён', + relationships: 'Связи', + systemBadge: 'Системный', + fieldCount: '{{count}} полей', + ungrouped: 'Без группы', + deleteConfirmTitle: 'Удалить объект?', + deleteConfirmMessage: 'Объект и все его поля будут удалены безвозвратно. Это действие невозможно отменить.', + }, + fieldDesigner: { + title: 'Дизайнер полей', + addField: 'Новое поле', + searchPlaceholder: 'Поиск полей…', + allTypes: 'Все типы', + noFields: 'Поля не найдены.', + fieldName: 'Имя API', + fieldLabel: 'Метка', + fieldType: 'Тип', + fieldGroup: 'Группа', + description: 'Описание', + required: 'Обязательное', + unique: 'Уникальное', + readOnly: 'Только чтение', + hidden: 'Скрытое', + indexed: 'Индексированное', + externalId: 'Внешний ID', + trackHistory: 'Отслеживание истории', + defaultValue: 'Значение по умолчанию', + placeholder: 'Подсказка', + referenceTo: 'Ссылка на', + formula: 'Формула', + options: 'Варианты', + addOption: 'Добавить вариант', + validationRules: 'Правила валидации', + addRule: 'Добавить правило', + systemBadge: 'Системное', + ungrouped: 'Общее', + deleteConfirmTitle: 'Удалить поле?', + deleteConfirmMessage: 'Поле будет удалено безвозвратно. Существующие данные будут потеряны.', + }, }, console: { title: 'Консоль ObjectStack', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 8ca7a558..737d8ddc 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -508,6 +508,58 @@ const zh = { modeLight: '浅色', modeDark: '深色', mobilePreview: '移动端预览', + objectManager: { + title: '对象管理器', + addObject: '新建对象', + searchPlaceholder: '搜索对象…', + noObjects: '未找到对象。', + objectName: 'API 名称', + objectLabel: '标签', + pluralLabel: '复数标签', + icon: '图标', + selectIcon: '选择图标…', + group: '分组', + noGroup: '无分组', + sortOrder: '排序', + enabled: '启用', + relationships: '关系', + systemBadge: '系统', + fieldCount: '{{count}} 个字段', + ungrouped: '未分组', + deleteConfirmTitle: '删除对象?', + deleteConfirmMessage: '这将永久删除该对象及其所有字段。此操作无法撤销。', + }, + fieldDesigner: { + title: '字段设计器', + addField: '新建字段', + searchPlaceholder: '搜索字段…', + allTypes: '所有类型', + noFields: '未找到字段。', + fieldName: 'API 名称', + fieldLabel: '标签', + fieldType: '类型', + fieldGroup: '分组', + description: '描述', + required: '必填', + unique: '唯一', + readOnly: '只读', + hidden: '隐藏', + indexed: '索引', + externalId: '外部 ID', + trackHistory: '追踪历史', + defaultValue: '默认值', + placeholder: '占位文本', + referenceTo: '引用对象', + formula: '公式', + options: '选项', + addOption: '添加选项', + validationRules: '验证规则', + addRule: '添加规则', + systemBadge: '系统', + ungrouped: '通用', + deleteConfirmTitle: '删除字段?', + deleteConfirmMessage: '这将永久删除该字段。此字段中的现有数据将丢失。', + }, }, console: { title: 'ObjectStack 控制台', From 7ffbb8b080e9665f988643c655d0df54c3d3d39a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:43:30 +0000 Subject: [PATCH 03/14] feat: implement ObjectManager and FieldDesigner visual designers - Add type definitions (ObjectDefinition, DesignerFieldDefinition, etc.) in @object-ui/types - Implement ObjectManager component with CRUD, search, grouping, property editing - Implement FieldDesigner component with CRUD, field types, validation rules, advanced properties - Add i18n keys for all 10 locales (en, zh, ja, ko, de, fr, es, pt, ru, ar) - Export new components and types from @object-ui/plugin-designer - Register components in ComponentRegistry Agent-Logs-Url: https://github.com/objectstack-ai/objectui/sessions/143938d4-431c-4d1e-9017-8534ade7ff71 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/i18n/src/locales/en.ts | 52 ++ .../plugin-designer/src/FieldDesigner.tsx | 846 ++++++++++++++++++ .../plugin-designer/src/ObjectManager.tsx | 527 +++++++++++ .../src/hooks/useDesignerTranslation.ts | 50 ++ packages/plugin-designer/src/index.tsx | 36 + packages/types/src/designer.ts | 166 ++++ packages/types/src/index.ts | 8 + 7 files changed, 1685 insertions(+) create mode 100644 packages/plugin-designer/src/FieldDesigner.tsx create mode 100644 packages/plugin-designer/src/ObjectManager.tsx diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 7742b908..166ac556 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -508,6 +508,58 @@ const en = { modeLight: 'Light', modeDark: 'Dark', mobilePreview: 'Mobile Preview', + objectManager: { + title: 'Object Manager', + addObject: 'New Object', + searchPlaceholder: 'Search objects…', + noObjects: 'No objects found.', + objectName: 'API Name', + objectLabel: 'Label', + pluralLabel: 'Plural Label', + icon: 'Icon', + selectIcon: 'Select icon…', + group: 'Group', + noGroup: 'No Group', + sortOrder: 'Sort Order', + enabled: 'Enabled', + relationships: 'Relationships', + systemBadge: 'System', + fieldCount: '{{count}} fields', + ungrouped: 'Ungrouped', + deleteConfirmTitle: 'Delete Object?', + deleteConfirmMessage: 'This will permanently delete the object and all its fields. This action cannot be undone.', + }, + fieldDesigner: { + title: 'Field Designer', + addField: 'New Field', + searchPlaceholder: 'Search fields…', + allTypes: 'All Types', + noFields: 'No fields found.', + fieldName: 'API Name', + fieldLabel: 'Label', + fieldType: 'Type', + fieldGroup: 'Group', + description: 'Description', + required: 'Required', + unique: 'Unique', + readOnly: 'Read Only', + hidden: 'Hidden', + indexed: 'Indexed', + externalId: 'External ID', + trackHistory: 'Track History', + defaultValue: 'Default Value', + placeholder: 'Placeholder', + referenceTo: 'Reference To', + formula: 'Formula', + options: 'Options', + addOption: 'Add Option', + validationRules: 'Validation Rules', + addRule: 'Add Rule', + systemBadge: 'System', + ungrouped: 'General', + deleteConfirmTitle: 'Delete Field?', + deleteConfirmMessage: 'This will permanently delete the field. Existing data in this field will be lost.', + }, }, console: { title: 'ObjectStack Console', diff --git a/packages/plugin-designer/src/FieldDesigner.tsx b/packages/plugin-designer/src/FieldDesigner.tsx new file mode 100644 index 00000000..a722545e --- /dev/null +++ b/packages/plugin-designer/src/FieldDesigner.tsx @@ -0,0 +1,846 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * FieldDesigner Component + * + * Enterprise-grade visual designer for configuring object fields. + * Supports CRUD operations on fields, advanced property editing, + * field grouping, sorting, validation rules, and system field display. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import type { DesignerFieldDefinition, DesignerFieldType, DesignerFieldOption, DesignerValidationRule } from '@object-ui/types'; +import { + Plus, + Search, + Trash2, + Edit3, + ChevronDown, + ChevronUp, + Columns3, + Settings2, + Eye, + EyeOff, + Lock, + Unlock, + Hash, + Type, + Calendar, + ToggleLeft, + ListOrdered, + Link2, + AtSign, + Phone, + Globe, + FileText, + Image, + Palette, + Code, + MapPin, + Star, + SlidersHorizontal, + GripVertical, + Shield, + History, + Key, +} from 'lucide-react'; +import { clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; +import { useDesignerTranslation } from './hooks/useDesignerTranslation'; +import { useConfirmDialog } from './hooks/useConfirmDialog'; +import { ConfirmDialog } from './components/ConfirmDialog'; + +function cn(...inputs: (string | undefined | false)[]) { + return twMerge(clsx(inputs)); +} + +// ============================================================================ +// Types +// ============================================================================ + +export interface FieldDesignerProps { + /** Object name this designer belongs to */ + objectName: string; + /** List of field definitions */ + fields: DesignerFieldDefinition[]; + /** Callback when fields change */ + onFieldsChange?: (fields: DesignerFieldDefinition[]) => void; + /** Read-only mode */ + readOnly?: boolean; + /** CSS class */ + className?: string; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const FIELD_TYPE_META: Record }> = { + text: { label: 'Text', Icon: Type }, + textarea: { label: 'Text Area', Icon: FileText }, + number: { label: 'Number', Icon: Hash }, + boolean: { label: 'Checkbox', Icon: ToggleLeft }, + date: { label: 'Date', Icon: Calendar }, + datetime: { label: 'Date/Time', Icon: Calendar }, + time: { label: 'Time', Icon: Calendar }, + select: { label: 'Picklist', Icon: ListOrdered }, + email: { label: 'Email', Icon: AtSign }, + phone: { label: 'Phone', Icon: Phone }, + url: { label: 'URL', Icon: Globe }, + password: { label: 'Password', Icon: Lock }, + currency: { label: 'Currency', Icon: Hash }, + percent: { label: 'Percent', Icon: Hash }, + lookup: { label: 'Lookup', Icon: Link2 }, + formula: { label: 'Formula', Icon: Code }, + autonumber: { label: 'Auto Number', Icon: Hash }, + file: { label: 'File', Icon: FileText }, + image: { label: 'Image', Icon: Image }, + markdown: { label: 'Markdown', Icon: FileText }, + html: { label: 'Rich Text', Icon: FileText }, + color: { label: 'Color', Icon: Palette }, + code: { label: 'Code', Icon: Code }, + location: { label: 'Location', Icon: MapPin }, + address: { label: 'Address', Icon: MapPin }, + rating: { label: 'Rating', Icon: Star }, + slider: { label: 'Slider', Icon: SlidersHorizontal }, +}; + +const ALL_FIELD_TYPES = Object.keys(FIELD_TYPE_META) as DesignerFieldType[]; + +// ============================================================================ +// Collapsible Section +// ============================================================================ + +interface SectionProps { + title: string; + icon: React.ReactNode; + defaultOpen?: boolean; + children: React.ReactNode; + badge?: string; +} + +function Section({ title, icon, defaultOpen = true, children, badge }: SectionProps) { + const [open, setOpen] = useState(defaultOpen); + + return ( +
+ + {open &&
{children}
} +
+ ); +} + +// ============================================================================ +// Field Property Editor (inline) +// ============================================================================ + +interface FieldEditorProps { + field: DesignerFieldDefinition; + onChange: (updated: DesignerFieldDefinition) => void; + readOnly: boolean; + t: (key: string, options?: Record) => string; +} + +function FieldEditor({ field, onChange, readOnly, t }: FieldEditorProps) { + const update = useCallback( + (partial: Partial) => { + onChange({ ...field, ...partial }); + }, + [field, onChange] + ); + + const addOption = useCallback(() => { + const options = field.options || []; + const newOption: DesignerFieldOption = { + label: `Option ${options.length + 1}`, + value: `option_${options.length + 1}`, + }; + update({ options: [...options, newOption] }); + }, [field.options, update]); + + const removeOption = useCallback( + (idx: number) => { + const options = [...(field.options || [])]; + options.splice(idx, 1); + update({ options }); + }, + [field.options, update] + ); + + const updateOption = useCallback( + (idx: number, partial: Partial) => { + const options = [...(field.options || [])]; + options[idx] = { ...options[idx], ...partial }; + update({ options }); + }, + [field.options, update] + ); + + const addValidationRule = useCallback(() => { + const rules = field.validationRules || []; + const newRule: DesignerValidationRule = { + type: 'minLength', + value: 0, + message: '', + }; + update({ validationRules: [...rules, newRule] }); + }, [field.validationRules, update]); + + const removeValidationRule = useCallback( + (idx: number) => { + const rules = [...(field.validationRules || [])]; + rules.splice(idx, 1); + update({ validationRules: rules }); + }, + [field.validationRules, update] + ); + + return ( +
+ {/* Name */} +
+ + update({ name: e.target.value })} + disabled={readOnly || field.isSystem} + data-testid="field-editor-name" + className="block w-full rounded-md border border-gray-300 px-2 py-1 text-xs shadow-sm outline-none transition-colors focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-100" + placeholder="api_name" + /> +
+ + {/* Label */} +
+ + update({ label: e.target.value })} + disabled={readOnly} + data-testid="field-editor-label" + className="block w-full rounded-md border border-gray-300 px-2 py-1 text-xs shadow-sm outline-none transition-colors focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-100" + placeholder="Display Label" + /> +
+ + {/* Type */} +
+ + +
+ + {/* Group */} +
+ + update({ group: e.target.value })} + disabled={readOnly} + data-testid="field-editor-group" + className="block w-full rounded-md border border-gray-300 px-2 py-1 text-xs shadow-sm outline-none transition-colors focus:border-blue-500 focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:bg-gray-100" + placeholder="Field Group" + /> +
+ + {/* Description */} +
+ +