Compare commits

..

27 commits
1.0.2 ... main

Author SHA1 Message Date
Sedat Öztürk
3ac72ad19b Docker düzenlemesi 2026-04-23 15:30:20 +03:00
Sedat Öztürk
c26f0cb5bc DbMigrator UI 2026-04-23 13:36:51 +03:00
Sedat Öztürk
3db9fc332b Template Button güncellemesi 2026-03-30 23:40:20 +03:00
Sedat ÖZTÜRK
e9ce256c07 Grid ve Form itemları için PlaceHolder tanımlaması 2026-03-30 15:05:25 +03:00
Sedat ÖZTÜRK
9fb838dcba RolePermission kopyalama 2026-03-30 13:53:50 +03:00
Sedat ÖZTÜRK
b0dd72ec57 Seeder düzenlemesi 2026-03-30 13:10:25 +03:00
Sedat ÖZTÜRK
9d5c5ccf09 Duplicate Record düzenlemesi 2026-03-30 12:16:28 +03:00
Sedat Öztürk
30be61f2c7 Duplicate Record 2026-03-30 06:15:42 +03:00
Sedat Öztürk
08c495943b RolePermission ve UserPermission css düzenlemesi 2026-03-29 15:30:57 +03:00
Sedat Öztürk
6177dbcf24 Restrictions düzenlemesi 2026-03-29 14:07:34 +03:00
Sedat Öztürk
87ba256bae LanguagesData temizliği 2026-03-29 11:59:07 +03:00
Sedat Öztürk
9e64976963 Seeder düzenlemeleri 2026-03-28 11:23:21 +03:00
Sedat Öztürk
130d35c377 Report Template View / Design 2026-03-28 10:04:13 +03:00
Sedat Öztürk
8c54c6590c Grid düzenlemeleri Css 2026-03-28 01:17:45 +03:00
Sedat Öztürk
27ff19ca0d Theme Configuration güncellemesi 2026-03-27 23:17:35 +03:00
Sedat ÖZTÜRK
f57fbda2d6 Theme Configuration 2026-03-27 16:49:15 +03:00
Sedat ÖZTÜRK
3baa7def61 AI Instructions 2026-03-25 08:52:55 +03:00
Sedat ÖZTÜRK
cf6ded1105 ActivityLog düzenlemesi 2026-03-24 09:38:55 +03:00
Sedat ÖZTÜRK
a7e8d7995b Menu ve Sql Query Manager Required 2026-03-23 10:25:00 +03:00
Sedat Öztürk
62f38a27a5 File Management ve AI Asistant güncellemeleri 2026-03-22 18:05:08 +03:00
Sedat Öztürk
17df35102d FormView, FormEdit ve FormNew için MenuIcon 2026-03-22 10:18:13 +03:00
Sedat Öztürk
203160fce0 Note ve AuditLog kısmı düzenlendi. 2026-03-22 03:56:24 +03:00
Sedat Öztürk
4943f78f89 TenantConnection String güncellemeleri 2026-03-21 23:34:14 +03:00
Sedat Öztürk
dd82d405ce Dosya yöneticisi Tenant path düzenlemesi 2026-03-21 23:04:57 +03:00
Sedat Öztürk
cb4a74bf81 Dosya Yöneticisi Resim güncellemeleri 2026-03-21 22:44:25 +03:00
Sedat Öztürk
401db7bfef Wizard tarafında kolaylaştırıcı özellikler 2026-03-21 21:51:27 +03:00
Sedat Öztürk
95b8b8e798 AiBot güncellemeleri 2026-03-21 21:10:40 +03:00
149 changed files with 4673 additions and 3125 deletions

711
.github/instructions/ai.instructions.md vendored Normal file
View file

@ -0,0 +1,711 @@
# Dynamic Low-Code Platform - AI Instruction and Operating Manual
## 1. Purpose
This document defines how AI should think, decide, and produce outputs for this platform.
Primary objective:
- Maximize delivery through runtime configuration.
- Minimize custom code.
- Preserve platform consistency, security, and tenant isolation.
Primary principle:
- Configuration first, code last.
---
## 2. Platform Scope
Core stack:
- Backend: C# .NET 9 + ABP 9.x
- Frontend: React 18 + DevExtreme
- Database: SQL Server (dynamic datasource support)
- Cache: Redis
- Background jobs: Hangfire + ABP Background Workers
System nature:
- Runtime configurable application engine
- Multi-tenant SaaS foundation
- Low-code / no-code first
---
## 3. Non-Negotiable Rules
1. Do not propose new custom React component/page development for standard feature requests.
2. Build new screens using platform configuration mechanisms.
3. Every proposal must include tenant and permission design.
4. Never bypass platform authorization patterns.
5. Never hardcode secrets, tenant ids, or connection strings.
Exception policy:
- Code-level React or backend development can be considered only if user explicitly requests code implementation and configuration path is insufficient.
- If exception is required, AI must explain why configuration-based options are not enough.
---
## 4. Decision Flow (Mandatory)
For every request, apply this order:
1. Dynamic configuration with existing ListForm ecosystem
2. SQL Query Manager + Custom Endpoint
3. Dynamic Service
4. Code change (last resort, justification required)
---
## 5. Architecture Guardrails
Backend (ABP):
- Respect modular boundaries.
- Prefer application services over ad-hoc endpoints.
- Keep permissions explicit and auditable.
Frontend (React + DevExtreme):
- Use existing dynamic view infrastructure.
- Keep component behavior metadata-driven.
- Preserve route, menu, and permission coherence.
Data:
- SQL-first for shaping, filtering, aggregation, reporting.
- Parameterized query patterns only.
- Ensure tenant-safe data access.
---
## 6. Dynamic Menu and Routing
- Menus are database driven.
- Routes are generated/managed dynamically.
- Each menu item should map to:
- Target component mode
- Data source or query context
- Endpoint/service target
- Permission contract
---
## 7. Dynamic List System
Driven by:
- ListForm
- ListFormFields
- ListFormCustomization (UserUiFilter, GridState, ServerJoin, ServerWhere)
- ListFormImport and ListFormImportExecute
- ListFormJsonRow operations
Capabilities:
- Dynamic columns, command column, banded columns
- Permission-aware visibility and action rendering
- Rich filtering, sorting, grouping, paging, searching
- Dynamic toolbar actions and URL/dialog/script command flows
- Lookup sources:
- StaticData
- Query
- WebService
- Cascading lookup parent-child behavior
- Dynamic validation, editor options, editor scripts
- Conditional formatting and style injection
- Grid state save/load/reset
- User filter save/apply/delete flows
- Import manager and export flows (xlsx, csv, pdf)
- SubForm integration from selected row context
- Remote operations and dynamic datasource rebinding
Supported editor types:
- dxAutocomplete
- dxCalendar
- dxCheckBox
- dxColorBox
- dxDateBox
- dxDateRangeBox
- dxDropDownBox
- dxGridBox
- dxHtmlEditor
- dxLookup
- dxNumberBox
- dxRadioGroup
- dxRangeSlider
- dxSelectBox
- dxSlider
- dxSwitch
- dxTagBox
- dxTextArea
- dxTextBox
---
## 8. Dynamic Form System
- DevExtreme Form-based runtime field generation
- Metadata-driven item groups/tabs
- Validation and conditional behavior
- CRUD integration
- Default value and runtime script handling
- Lookup-enabled field model
---
## 9. Dynamic UI Components
Supported component families:
- DataGrid
- PivotGrid
- Form
- Chart
- Scheduler
- Gantt
- TreeList / Tree view
- SubForm tabs (List, Tree, Gantt, Scheduler, Form, Chart)
- Widget Group (dashboard KPI cards)
Runtime UI capabilities:
- Per-list layout switching:
- Grid
- Pivot
- Tree
- Chart
- Gantt
- Scheduler
- Per-user layout persistence
- Lazy loading and view preloading
- Shared dynamic datasource pipeline across layouts
- Shared filter/search/deep-link behavior
- Runtime refresh and query rebind
- Runtime state lifecycle:
- Save state
- Load state
- Reset state
- Runtime import/export actions from UI flows
- Popup and fullscreen editing scenarios
- SubForm relation-aware context propagation and tab navigation
---
## 10. Dynamic Component Development Policy
Allowed approach:
- Configure and bind existing dynamic component infrastructure.
Disallowed by default:
- New custom React pages/components for normal business requirements.
If user requests custom code explicitly:
- Provide a warning that low-code path is preferred.
- Offer configuration-first alternative first.
---
## 11. Integrations
Notification channels:
- Sms
- Mail
- Rocket
- Desktop
- UiActivity
- UiToast
- WhatsApp
- Telegram
Sender module:
- Email (ABP Emailing + Amazon SES)
- SMS (ABP Sms + Posta Guvercini)
- Rocket.Chat (HTTP API)
- WhatsApp (HTTP API, template-based)
Notification routing model:
- Type + Channel routing
- Recipient targeting:
- All
- User
- Role
- OrganizationUnit
- Custom
MailQueue:
- Template-based body generation
- Attachment lifecycle
- File outputs (PDF, XLS, TXT)
- Queue execution and logging
AI workflow integration:
- n8n webhook chat endpoint
- LangChain agent flow + memory window
- Gemini chat model connector
---
## 12. Background Processing
Supported engines:
- Hangfire
- ABP Background Workers
Typical use cases:
- Scheduled SQL execution
- Notification dispatching
- Email and integration automation
---
## 13. Security and Compliance Rules
- Enforce RBAC and permission-driven visibility at all layers.
- Always include permission definitions in feature design.
- Never output real credentials, tokens, keys, or secrets.
- Use placeholders in examples.
- Maintain tenant isolation in every query and action.
---
## 14. Required AI Output Contract
For each implementation proposal, AI output must include:
1. Goal
2. Decision flow result (which step used)
3. Artifacts to configure
4. SQL/query/endpoint design (if needed)
5. Menu + route + component mapping
6. Permission and role mapping
7. Tenant isolation notes
8. Validation and test checklist
9. Rollback strategy
---
## 15. Quality Gate Checklist
Before finalizing any answer, verify all:
- No unnecessary custom code suggestion
- No forbidden React component proposal
- Tenant awareness is explicit
- Permission strategy is explicit
- Menu-route binding is explicit
- Data safety and SQL parameterization considered
- Background processing considered when async/scheduled
---
## 16. Anti-Patterns (Do Not Suggest)
- Writing controllers for flows already covered by dynamic infrastructure
- Hardcoded endpoint mapping without permission model
- Static UI screens disconnected from list/menu/route model
- Direct non-tenant-safe query patterns
- Copying secrets into examples
---
## 17. Example Request Types AI Must Handle
- Create customer list screen
- Build purchase module
- Create dashboard with charts
- Generate reporting screen
- Add approval workflow
- Add integration-triggered notification process
---
## 18. Escalation Strategy
When configuration cannot satisfy requirement:
1. Try SQL-based design
2. Try Custom Endpoint
3. Try Dynamic Service
4. Propose minimal code change with explicit justification
---
## 19. Final Rule
This platform is not a traditional code-first app.
It is a runtime configurable low-code engine.
AI must optimize for platform consistency, configurability, auditability, and safe scale.
---
## 20. Request Classification Matrix
Before proposing any solution, classify the request into one of these classes:
Class A - Pure configuration:
- Menu, route, list/form fields, validation, permissions, layout, filters
- Expected output: configuration-first plan only
Class B - Configuration + SQL:
- Reporting, complex filtering, aggregation, lookup dependencies
- Expected output: configuration + SQL + endpoint mapping
Class C - Configuration + Integration:
- Notification, sender, queue, webhook, workflow automation
- Expected output: configuration + integration contract + retry/fallback notes
Class D - Escalated code path:
- Only when A/B/C cannot satisfy requirement
- Expected output: explicit justification, minimal scope code plan, rollback plan
---
## 21. Response Playbooks
### 21.1 New Screen Playbook
1. Define business objective and actor roles.
2. Define ListForm and ListFormFields structure.
3. Define menu entry and route mapping.
4. Define permissions (R/C/U/D/E) and role mapping.
5. Define datasource and query strategy.
6. Define runtime filters, lookup, validations.
7. Define import/export and state requirements.
8. Define acceptance checklist.
### 21.2 Workflow/Approval Playbook
1. Define states and transition rules.
2. Define transition permissions by role.
3. Define transition side-effects (notification, queue, endpoint call).
4. Define audit log and failure handling.
5. Define rollback/compensation behavior.
### 21.3 Report/Dashboard Playbook
1. Define KPIs and dimensions.
2. Define SQL shaping and aggregation logic.
3. Define chart/pivot configuration.
4. Define date/tenant filters.
5. Define export needs and performance constraints.
---
## 22. Mandatory Acceptance Criteria Template
Every final proposal must include a clear, testable acceptance list:
1. Feature can be enabled via configuration.
2. Tenant boundaries are preserved.
3. Required permissions block unauthorized access.
4. Menu-route-screen path is navigable.
5. Data operations are parameterized and safe.
6. Runtime filters and state behaviors work.
7. Integration events (if any) are observable and retry-safe.
8. No unnecessary custom code introduced.
---
## 23. AI Output Templates
### 23.1 Configuration-only Output Template
1. Goal
2. Configuration artifacts
3. Menu/route mapping
4. Permission mapping
5. Validation checklist
### 23.2 Configuration + SQL Output Template
1. Goal
2. Required configuration artifacts
3. SQL design (inputs, outputs, filter parameters)
4. Endpoint/service mapping
5. Security and tenant notes
6. Validation checklist
### 23.3 Escalated Code Output Template
1. Why configuration is insufficient
2. Minimal code scope
3. Compatibility with existing dynamic architecture
4. Migration and rollback plan
5. Risk and test plan
---
## 24. Performance and Scalability Rules
- Prefer server-side filtering/sorting/paging for large datasets.
- Avoid unbounded result sets in dynamic queries.
- Use indexing-aware query patterns for reporting screens.
- Keep chart/pivot queries aggregation-focused.
- Separate interactive screen queries from heavy export queries.
- Use background jobs for long-running batch operations.
---
## 25. Observability and Audit Rules
- Log key operations at list/form/integration boundaries.
- Preserve who/when/what audit metadata for critical changes.
- Capture integration failures with retry reason and payload context.
- Keep user-facing messages simple, logs detailed.
- Ensure state changes are diagnosable for support teams.
---
## 26. Error Handling Policy
- Never expose raw secrets or internal stack details to end users.
- Return actionable, localized, user-level messages.
- Keep technical diagnostics in logs.
- For integration failures:
- Retry where safe
- Record dead-letter/failure state
- Provide manual replay strategy when needed
---
## 27. Change Management Policy
- Prefer additive changes over breaking modifications.
- Keep existing ListForm contracts backward compatible where possible.
- Document behavior changes in rollout notes.
- For destructive changes, require explicit migration path.
---
## 28. Definition of Done for AI Proposals
A proposal is complete only if all are present:
1. Decision flow step selected and justified.
2. Configuration artifacts clearly listed.
3. Permission and tenant design included.
4. Menu-route-component mapping included.
5. Validation checklist included.
6. Risk/rollback notes included.
---
## 29. Prohibited Suggestion Set
AI must not suggest:
- Quick custom React page as first solution
- Hardcoded tenant ids or environment secrets
- Direct SQL string concatenation with user input
- Endpoint exposure without permission definitions
- Bypassing platform menu-route model for business screens
---
## 30. Final Governance Statement
This manual is authoritative for AI behavior in this repository.
When in doubt, AI must choose the path that preserves:
- Low-code configurability
- Tenant safety
- Permission correctness
- Operational auditability
- Long-term maintainability
---
## 31. Seeder-Driven Low-Code Development Guide (Authoritative)
AI must learn and teach implementation flow primarily from these seed assets:
- api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs
- api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json
- api/src/Sozsoft.Platform.DbMigrator/Seeds/PermissionsData.json
- api/src/Sozsoft.Platform.DbMigrator/Seeds/HostData.json
- api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json
If user asks "how to add a new module/screen", AI must answer with this exact operational sequence.
### 31.1 Step 1 - Create ListForm definition
Define a new `ListForm` with at least:
- `ListFormCode`, `Name`, `Title`
- `DataSourceCode`
- `SelectCommandType` + `SelectCommand`
- `KeyFieldName` + `KeyFieldDbSourceType`
- `PermissionJson` (create/read/update/delete/export/import/note)
- `EditingOptionJson` + `EditingFormJson`
- `FilterRowJson`, `HeaderFilterJson`, `SearchPanelJson`, `GroupPanelJson`
- `SelectionJson`, `ColumnOptionJson`, `PagerOptionJson`
- `InsertServiceAddress`, `UpdateServiceAddress`, `DeleteCommand`
For parent-child scenarios, define `SubFormsJson` relation mapping (ParentFieldName -> ChildFieldName, DbType).
### 31.2 Step 2 - Create ListFormFields
For each field, define runtime behavior through `ListFormField` records:
- Data binding: field name, db type, source
- UI behavior: visibility, order, width, grouping, fixed/band settings
- Editing behavior: editor type (`dxTextBox`, `dxSelectBox`, `dxNumberBox`, etc.), required, options
- Lookup behavior: data source type, display/value members, cascade rules
- Validation rules and default values
- Command/action columns where needed
AI must prioritize metadata-based field behavior instead of suggesting custom React forms.
### 31.3 Step 3 - Add menu and route mapping
From `MenusData.json` patterns, AI must define both:
- `Routes` entry:
- `key`, `path`, `componentPath`, `routeType`, `authority`
- `Menus` entry:
- `ParentCode`, `Code`, `DisplayName`, `Url`, `Icon`, `RequiredPermissionName`, `Order`
For dynamic list screens, base route pattern is:
- `/admin/list/{ListFormCode}`
AI must ensure menu URL and route path point to same screen contract.
### 31.4 Step 4 - Add permissions
From `PermissionsData.json` patterns, AI must define:
- Permission group (if missing)
- Permission definitions for feature root and actions
- Menu permission binding (`RequiredPermissionName`)
Minimum action set suggestion:
- `.Default`
- `.Create`
- `.Update`
- `.Delete`
- `.Export`
- `.Import`
- `.Note`
AI must never propose menu/route without permission contract.
### 31.5 Step 5 - Add settings/integration dependencies
From `HostData.json` patterns, AI must define required settings when feature depends on external services:
- Sender credentials and endpoints (sms/mail/whatsapp/rocket)
- AiBot integration endpoint and activation state
- Feature-specific setting keys, providers, encryption flags
If integration required and setting missing, AI must explicitly add "blocking prerequisite" note.
### 31.6 Step 6 - Data and service contract
AI must produce one of:
- Direct table/view select command
- Parameterized SQL via managed query
- Custom endpoint / dynamic service
AI must include tenant-safe filters and avoid string concatenation.
### 31.7 Step 7 - Delivery checklist
For every new low-code feature, AI output must list:
1. New `ListForm` code and purpose
2. `ListFormField` set (critical columns/editors/lookups)
3. Route record
4. Menu record
5. Permission records
6. Required settings/integrations
7. Validation and rollback notes
If this 7-item list is incomplete, proposal is not accepted.
### 31.8 AI response style for implementation requests
When user asks for a screen/module, AI must answer in this order:
1. Decision flow class (A/B/C/D)
2. Seeder-style artifacts to add (ListForm, Fields, Menu, Route, Permission, Setting)
3. If needed, SQL/endpoint contract
4. Test and rollback checklist
AI should produce practical, copy-adaptable artifact definitions and avoid abstract-only explanations.
---
## 32. Mandatory Default Behaviors (Do Not Ask Repeatedly)
The following defaults are mandatory unless user explicitly overrides them.
### 32.1 SQL Table Designer default column behavior
When user requests creating a table and only specifies business columns (for example `FullName`, `Phone`), AI must still ensure platform-standard technical columns are included by default in SQL Query Manager table design flow:
- `TenantId` (multi-tenant compatibility)
- Full audited set:
- `Id`
- `CreationTime`
- `CreatorId`
- `LastModificationTime`
- `LastModifierId`
- `IsDeleted`
- `DeletionTime`
- `DeleterId`
Rules:
- Do not require user to explicitly request these columns each time.
- If user explicitly says "no tenant" or "no audit", then respect that override.
- Primary key strategy must remain compatible with existing index/key policy.
### 32.2 Wizard menu parent fallback behavior
When creating menu via Wizard/ListForm and user does not explicitly specify parent menu:
- Do not auto-place under `Definitions` by default.
- Create (or use) a dedicated new top-level parent menu for that feature/module.
- Assign top-level menu `Order` as next available order (`max(Order) + 1`) among root menus.
- Place the generated list/menu item under this newly created parent.
Rules:
- `Definitions` can be used only when user explicitly selects it.
- Permission contract must be created/bound for both parent and child menu items.
- Route/menu consistency remains mandatory (`Url` and screen contract must match).
### 32.3 AI enforcement requirement
AI must proactively apply these defaults in:
- implementation proposals,
- code/seed changes,
- migration and wizard behavior recommendations.
If AI output omits these defaults without explicit user override, output is non-compliant.

View file

@ -0,0 +1,149 @@
# Sozsoft Platform Module Implementation Rules & Instructions
## Purpose
This document summarizes the rules, standards, and step-by-step instructions for implementing any module (e.g., CRM, MRP, HR) with a sample list (e.g., Opportunity, Order) in the Sozsoft Platform. Replace all placeholders (e.g., {Modul}, {Liste}, {Entity}) with your actual module, list, or entity names. Use as a reference for future development and as a prompt guide for similar tasks.
---
## General Principles
- **Configuration First:** Always prefer platform configuration (menus, permissions, forms, localization) over custom code.
- **Modularization:** Each module (e.g., {Modul}) must have its own seeder, permission group, and localization entries.
- **Naming Conventions:**
- Table names: `{Modul}_T_{Entity}` (e.g., `Mrp_T_Order`)
- Menu/permission keys: `App.{Modul}`, `App.{Modul}.{Liste}`
- **Tenant Isolation:** All data, forms, and permissions must be tenant-aware.
- **Localization:** Every menu, list, and field must have a corresponding entry in `LanguagesData.json`.
- **No Redundant Code:** Avoid duplicating permission grants or seeding logic across modules.
- **SeedConsts Rule:** For every new module/entity, add a constant to `SeedConsts` (e.g., `public static class {Modul}` and `public const string {Liste}`) if it does not already exist. Never duplicate existing constants.
- **DeleteCommand Rule:** In all `ListFormSeeder_{Modul}.cs` files, the `DeleteCommand` property **must** use the `DefaultDeleteCommand` function, e.g., `DeleteCommand = DefaultDeleteCommand("{TabloAdı}")`. Never use a raw SQL string directly for `DeleteCommand`.
---
## Special File Handling Rules
- **MenusData.json:**
- Never modify the `Routes` section directly. Only `MenuGroups` and `Menus` sections can be changed for menu additions or updates. All menu additions must use the platform's configuration mechanisms or be added to `MenuGroups` and `Menus` as required by platform design.
- **LanguagesData.json:**
- When adding a new key, always check if the key already exists. Only add the key if it does not exist to prevent duplicates.
---
## File-by-File Change Summary
### 1. MenusData.json
- **{Modul} menu** added as a top-level menu with `ShortName: {Modul}`.
- **{Liste}** added as a child menu under {Modul}.
### 2. PermissionsData.json
- **{Modul} permissions** grouped under `App.{Modul}`.
- **{Liste} permissions** nested under {Modul} group.
### 3. ListFormSeeder_Administration.cs
- **Removed** {Liste} seeding logic (moved to {Modul} seeder).
### 4. ListFormSeeder_{Modul}.cs
- **Created** new seeder file for {Modul}.
- **Seeds** {Liste} list-form, referencing `{Modul}_T_{Entity}`.
### 5. SqlTables.sql
- **Renamed** {Liste} table to `{Modul}_T_{Entity}`.
- **Updated** all related constraints and references.
### 6. PlatformIdentityDataSeeder.cs
- **Removed** redundant {Modul} permission grant logic (now handled by PermissionsData.json).
### 7. LanguagesData.json
- **Added** localization entries for:
- {Modul} menu and {Liste} list
- {Liste} list fields (e.g., `FullName`, `Phone`)
---
## Step-by-Step Instructions (Prompt Examples)
### 1. Add a New Module (e.g., {Modul})
```
Yeni bir modül ekle (ör: {Modul}):
- MenusData.json'a kök menü olarak ekle
- PermissionsData.json'da ayrı bir PermissionGroup oluştur
- ListFormSeeder_{Modul}.cs dosyası oluştur ve list-form seed'ini buraya taşı
- SqlTables.sql'de tabloyu {Modul}_T_{Entity} olarak adlandır
- LanguagesData.json'a menü, liste ve alan çevirilerini ekle
```
### 2. Add a New List Under a Module
```
Yeni bir liste ekle (ör: {Liste}):
- {Modul} ana menüsünün altına ekle
- ListFormSeeder_{Modul}.cs dosyasına seed kodunu ekle
- SqlTables.sql'de tabloyu {Modul}_T_{Entity} olarak oluştur
- PermissionsData.json'da ilgili izinleri {Modul} grubuna ekle
- LanguagesData.json'a liste ve alan çevirilerini ekle
```
### 3. Enforce Tenant Isolation and Full Audit
```
Tablo ve list-form için tenant izolasyonu ve full-audit alanlarını ekle:
- Tabloya TenantId, CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeleterId, DeletionTime alanlarını ekle
- List-form seed'inde tenant-aware ayarları kontrol et
```
### 4. Add/Update Localization
```
Yeni menü, liste veya alan eklediğinde LanguagesData.json'a şu şekilde ekle:
- App.{Modul}
- App.{Modul}.{Liste}
- App.Listform.ListformField.{Alan}
```
### 5. SeedConsts and DeleteCommand Rules
```
- SeedConsts.cs dosyasına, yeni modül veya entity için (ör: public static class {Modul} ve altında public const string {Liste}) sabit ekle. Eğer zaten varsa tekrar ekleme.
- ListFormSeeder_{Modul}.cs dosyalarında DeleteCommand satırı **her zaman** DefaultDeleteCommand fonksiyonu ile olmalı:
DeleteCommand = DefaultDeleteCommand("{TabloAdı}")
- DeleteCommand'da doğrudan SQL stringi **kullanma**.
```
### 5. Remove Redundant or Incorrect Code
```
- Farklı modüllerde aynı izin veya seed kodu varsa, sadece ilgili modülde bırak
- PlatformIdentityDataSeeder.cs'de Permission grant kodunu kaldır, PermissionsData.json'dan yönet
```
---
## Validation Checklist
- [ ] Menü ve izinler doğru hiyerarşide mi?
- [ ] List-form seed'leri doğru modül dosyasında mı?
- [ ] Tablo isimleri ve referansları standartlara uygun mu? (örn: {Modul}_T_{Entity})
- [ ] LanguagesData.json'da tüm yeni menü, liste ve alanlar var mı?
- [ ] Redundant kod veya izin grant'ı var mı?
- [ ] Seed ve migration sonrası UI'da çeviriler doğru görünüyor mu?
- [ ] SeedConsts.cs dosyasında ilgili sabitler var mı, tekrar eklenmemiş mi?
- [ ] ListFormSeeder dosyalarında DeleteCommand satırı DefaultDeleteCommand fonksiyonu ile mi atanmış?
---
## Rollback Strategy
- Değişiklikleri modül bazında geri al (örn: sadece {Modul} ile ilgili dosyaları revert et)
- JSON dosyalarında eski anahtarları ve çevirileri sil
- Seeder ve migration dosyalarını eski haline getir
---
## Quick Prompts for Future Use
- "Yeni bir modül ekle ve tüm standartlara göre yapılandır."
- "Yeni bir liste ekle, tenant izolasyonu ve çevirileriyle birlikte."
- "Mevcut bir modülde eksik olan çeviri veya izinleri tamamla."
- "Tüm {Modul} ile ilgili seed ve izinleri modüler yapıya uygun şekilde güncelle."
---
> **Not:** Tüm işlemlerden sonra seed'leri çalıştırıp UI'da menü ve çevirileri kontrol etmeyi unutma.

View file

@ -44,6 +44,7 @@ COPY "src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkC
COPY "src/Sozsoft.Platform.HttpApi/Sozsoft.Platform.HttpApi.csproj" "src/Sozsoft.Platform.HttpApi/"
COPY "src/Sozsoft.Platform.HttpApi.Client/Sozsoft.Platform.HttpApi.Client.csproj" "src/Sozsoft.Platform.HttpApi.Client/"
COPY "src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj" "src/Sozsoft.Platform.HttpApi.Host/"
COPY "src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj" "src/Sozsoft.Platform.DbMigrator/"
COPY "test/Sozsoft.Platform.EntityFrameworkCore.Tests/Sozsoft.Platform.EntityFrameworkCore.Tests.csproj" "test/Sozsoft.Platform.EntityFrameworkCore.Tests/"
COPY "test/Sozsoft.Platform.TestBase/Sozsoft.Platform.TestBase.csproj" "test/Sozsoft.Platform.TestBase/"
RUN dotnet restore "src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj"
@ -51,6 +52,7 @@ RUN dotnet restore "src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.H
COPY . .
RUN mkdir -p publish
RUN dotnet publish "src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj" -c Release -o /app/publish --no-restore
RUN dotnet publish "src/Sozsoft.Platform.DbMigrator/Sozsoft.Platform.DbMigrator.csproj" -c Release -o /app/migrator
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
@ -97,4 +99,8 @@ EXPOSE 443
WORKDIR /srv/app
COPY --from=build /app/publish .
# Migrator publish çıktısını Setup modunun çağırabilmesi için kopyala
COPY --from=build /app/migrator /srv/Sozsoft.Platform.DbMigrator
ENTRYPOINT ["./Sozsoft.Platform.HttpApi.Host"]

View file

@ -15,6 +15,8 @@ public class AuditLogDto : EntityDto<Guid>
public string ApplicationName { get; set; }
public Guid? UserId { get; protected set; }
public string UserName { get; protected set; }
public Guid? TenantId { get; protected set; }
public string TenantName { get; protected set; }
public DateTime ExecutionTime { get; protected set; }
public int ExecutionDuration { get; protected set; }
public string ClientIpAddress { get; protected set; }

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Data;
using System.Text.Json;
using System.Text.Json.Serialization;
using Sozsoft.Platform.Enums;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.ListForms;
@ -14,6 +13,7 @@ public class ColumnFormatDto : AuditedEntityDto<Guid>
public string FieldName { get; set; }
public string CaptionName { get; set; }
public string PlaceHolder { get; set; }
public bool ReadOnly { get; set; }
public bool Visible { get; set; } // select sorgusuna dahildir fakat ekranda gosterilmez, kolon secicinin icerisinde bulunur
public bool IsActive { get; set; } // sadece isActive olan alanlar sorguya dahil edilir

View file

@ -18,6 +18,7 @@ public class GridEditingDto
public bool AllowDeleting { get; set; } = false;
public bool AllowAllDeleting { get; set; } = false;
public bool AllowAdding { get; set; } = false;
public bool AllowDuplicate { get; set; } = false;
public bool UseIcons { get; set; } = false;
public bool ConfirmDelete { get; set; } = true;
/// <summary>Accepted Values: 'first' | 'last' | 'pageBottom' | 'pageTop' | 'viewportBottom' | 'viewportTop'

View file

@ -18,7 +18,7 @@ public interface IAuditLogAppService
}
[Authorize(AppCodes.AuditLogs)]
[Authorize(AppCodes.IdentityManagement.AuditLogs)]
public class AuditLogAppService
: CrudAppService<AuditLog, AuditLogDto, Guid>
, IAuditLogAppService

View file

@ -25,17 +25,16 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
private readonly BlobManager _blobContainer;
private readonly IConfiguration _configuration;
private const string FileMetadataSuffix = ".metadata.json";
private const string FolderMarkerSuffix = ".folder";
private const string IndexFileName = "index.json";
// Protected system folders that cannot be deleted, renamed, or moved
private static readonly HashSet<string> ProtectedFolders = new(StringComparer.OrdinalIgnoreCase)
{
BlobContainerNames.Avatar,
BlobContainerNames.Import,
BlobContainerNames.Note,
BlobContainerNames.Intranet
// BlobContainerNames.Avatar,
// BlobContainerNames.Import,
// BlobContainerNames.Note,
// BlobContainerNames.Intranet
};
public FileManagementAppService(
@ -49,8 +48,52 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
_configuration = configuration;
}
private const string HostFolderName = "host";
private const string TenantsFolderName = "tenants";
private string GetTenantPrefix(string tenantId) => $"tenants/{tenantId}/";
private static bool IsHostTenant(string tenantId) =>
tenantId.Equals(HostFolderName, StringComparison.OrdinalIgnoreCase);
private static string ToSystemPath(string path) =>
path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
private static string GetCdnTenantRootPath(string cdnBasePath, string effectiveTenantId)
{
return IsHostTenant(effectiveTenantId)
? Path.Combine(cdnBasePath, HostFolderName)
: Path.Combine(cdnBasePath, TenantsFolderName, effectiveTenantId);
}
private static string NormalizeExtension(string? extensionOrFileName)
{
if (string.IsNullOrWhiteSpace(extensionOrFileName))
{
return string.Empty;
}
var ext = extensionOrFileName.Trim();
// If a path was provided, extract extension from the file name
if (ext.Contains('/') || ext.Contains('\\'))
{
ext = Path.GetExtension(ext);
}
else if (!ext.StartsWith('.') && ext.Contains('.'))
{
// File name like "photo.jpg" (but not an extension like ".jpg")
ext = Path.GetExtension(ext);
}
if (string.IsNullOrWhiteSpace(ext))
{
return string.Empty;
}
return ext.Trim().TrimStart('.').ToLowerInvariant();
}
private string GetEffectiveTenantId(string? inputTenantId) =>
!string.IsNullOrEmpty(inputTenantId) ? inputTenantId : (_currentTenant.Id?.ToString() ?? "host");
@ -75,16 +118,12 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var rootFolder = pathParts[0];
var isProtected = ProtectedFolders.Contains(rootFolder);
Logger.LogInformation($"IsProtectedFolder - Path: '{path}', RootFolder: '{rootFolder}', IsProtected: {isProtected}");
Logger.LogInformation($"Protected folders: {string.Join(", ", ProtectedFolders)}");
return isProtected;
}
private void ValidateNotProtectedFolder(string id, string operation)
{
var decodedPath = DecodeIdAsPath(id);
Logger.LogInformation($"ValidateNotProtectedFolder - ID: {id}, DecodedPath: {decodedPath}, Operation: {operation}");
if (IsProtectedFolder(decodedPath))
{
@ -92,8 +131,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Logger.LogWarning($"Blocked {operation} operation on protected folder: {folderName}");
throw new UserFriendlyException($"Cannot {operation} system folder '{folderName}'. This folder is protected.");
}
Logger.LogInformation($"Folder {decodedPath} is not protected, allowing {operation}");
}
private async Task<List<FileMetadata>> GetFolderIndexAsync(string? parentId, string tenantId)
@ -112,11 +149,11 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return items;
}
var fullPath = Path.Combine(cdnBasePath, tenantId);
var fullPath = GetCdnTenantRootPath(cdnBasePath, tenantId);
if (!string.IsNullOrEmpty(folderPath))
{
fullPath = Path.Combine(fullPath, folderPath);
fullPath = Path.Combine(fullPath, ToSystemPath(folderPath));
}
try
@ -170,6 +207,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var fileInfo = new FileInfo(file);
var relativePath = string.IsNullOrEmpty(folderPath) ? fileInfo.Name : $"{folderPath}/{fileInfo.Name}";
var normalizedExtension = NormalizeExtension(fileInfo.Extension);
items.Add(new FileMetadata
{
Id = EncodePathAsId(relativePath),
@ -181,7 +220,9 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Path = relativePath,
ParentId = folderPath ?? "",
IsReadOnly = false,
TenantId = tenantId == "host" ? null : tenantId
TenantId = tenantId == "host" ? null : tenantId,
Extension = normalizedExtension,
MimeType = GetMimeType(normalizedExtension)
});
}
@ -264,13 +305,13 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}
var tenantId = GetEffectiveTenantId(input.TenantId);
var parentPath = Path.Combine(cdnBasePath, tenantId);
var parentPath = GetCdnTenantRootPath(cdnBasePath, tenantId);
string? decodedParentId = null;
if (!string.IsNullOrEmpty(input.ParentId))
{
decodedParentId = DecodeIdAsPath(input.ParentId);
parentPath = Path.Combine(parentPath, decodedParentId);
parentPath = Path.Combine(parentPath, ToSystemPath(decodedParentId));
}
var folderPath = Path.Combine(parentPath, input.Name);
@ -351,11 +392,11 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
throw new UserFriendlyException("CDN path is not configured");
}
var fullCdnPath = Path.Combine(cdnBasePath, tenantId);
var fullCdnPath = GetCdnTenantRootPath(cdnBasePath, tenantId);
if (!string.IsNullOrEmpty(decodedParentId))
{
fullCdnPath = Path.Combine(fullCdnPath, decodedParentId);
fullCdnPath = Path.Combine(fullCdnPath, ToSystemPath(decodedParentId));
}
// Dizini oluştur
@ -379,6 +420,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}
var fileInfo = new FileInfo(uniqueFileName);
var normalizedExtension = NormalizeExtension(fileInfo.Extension);
var metadata = new FileMetadata
{
@ -386,8 +428,8 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
Name = uniqueFileName,
Type = "file",
Size = fileSize,
Extension = fileInfo.Extension,
MimeType = GetMimeType(fileInfo.Extension),
Extension = normalizedExtension,
MimeType = GetMimeType(normalizedExtension),
CreatedAt = DateTime.UtcNow,
ModifiedAt = DateTime.UtcNow,
Path = filePath,
@ -565,7 +607,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var effectiveTenantId = GetEffectiveTenantId(tenantId);
var actualPath = DecodeIdAsPath(id);
var fullPath = Path.Combine(cdnBasePath, effectiveTenantId, actualPath);
var fullPath = Path.Combine(GetCdnTenantRootPath(cdnBasePath, effectiveTenantId), ToSystemPath(actualPath));
if (Directory.Exists(fullPath))
{
@ -608,7 +650,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
ValidateNotProtectedFolder(itemId, "delete");
var actualPath = DecodeIdAsPath(itemId);
var fullPath = Path.Combine(cdnBasePath, tenantId, actualPath);
var fullPath = Path.Combine(GetCdnTenantRootPath(cdnBasePath, tenantId), ToSystemPath(actualPath));
if (Directory.Exists(fullPath))
{
@ -652,7 +694,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}
var tenantId = GetEffectiveTenantId(input.TenantId);
var basePath = Path.Combine(cdnBasePath, tenantId);
var basePath = GetCdnTenantRootPath(cdnBasePath, tenantId);
string? targetPath = null;
if (!string.IsNullOrEmpty(input.TargetFolderId))
@ -668,14 +710,14 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
try
{
var sourcePath = DecodeIdAsPath(itemId);
var sourceFullPath = Path.Combine(basePath, sourcePath);
var sourceFullPath = Path.Combine(basePath, ToSystemPath(sourcePath));
// Get source item name
var sourceItemName = Path.GetFileName(sourcePath);
// Generate unique name if item already exists in target
var targetItemPath = string.IsNullOrEmpty(targetPath) ? sourceItemName : $"{targetPath}/{sourceItemName}";
var targetFullPath = Path.Combine(basePath, targetItemPath);
var targetFullPath = Path.Combine(basePath, ToSystemPath(targetItemPath));
var uniqueTargetPath = GetUniqueItemPath(targetFullPath, sourceItemName);
var finalTargetPath = uniqueTargetPath.Replace(basePath + Path.DirectorySeparatorChar, "").Replace(Path.DirectorySeparatorChar, '/');
@ -711,7 +753,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
File.Copy(sourceFullPath, uniqueTargetPath);
var fileInfo = new FileInfo(uniqueTargetPath);
var extension = fileInfo.Extension;
var extension = NormalizeExtension(fileInfo.Extension);
copiedItems.Add(new FileItemDto
{
@ -763,7 +805,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}
var tenantId = GetEffectiveTenantId(input.TenantId);
var basePath = Path.Combine(cdnBasePath, tenantId);
var basePath = GetCdnTenantRootPath(cdnBasePath, tenantId);
string? targetPath = null;
if (!string.IsNullOrEmpty(input.TargetFolderId))
@ -782,14 +824,14 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
ValidateNotProtectedFolder(itemId, "move");
var sourcePath = DecodeIdAsPath(itemId);
var sourceFullPath = Path.Combine(basePath, sourcePath);
var sourceFullPath = Path.Combine(basePath, ToSystemPath(sourcePath));
// Get source item name
var sourceItemName = Path.GetFileName(sourcePath);
// Generate target path
var targetItemPath = string.IsNullOrEmpty(targetPath) ? sourceItemName : $"{targetPath}/{sourceItemName}";
var targetFullPath = Path.Combine(basePath, targetItemPath);
var targetFullPath = Path.Combine(basePath, ToSystemPath(targetItemPath));
// Check if moving to same location
if (Path.GetFullPath(sourceFullPath) == Path.GetFullPath(targetFullPath))
@ -839,7 +881,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
File.Move(sourceFullPath, uniqueTargetPath);
var fileInfo = new FileInfo(uniqueTargetPath);
var extension = fileInfo.Extension;
var extension = NormalizeExtension(fileInfo.Extension);
movedItems.Add(new FileItemDto
{
@ -887,7 +929,7 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var effectiveTenantId = GetEffectiveTenantId(tenantId);
var actualPath = DecodeIdAsPath(id);
var fullFilePath = Path.Combine(cdnBasePath, effectiveTenantId, actualPath);
var fullFilePath = Path.Combine(GetCdnTenantRootPath(cdnBasePath, effectiveTenantId), ToSystemPath(actualPath));
if (!File.Exists(fullFilePath))
{
@ -1013,7 +1055,16 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
private string GetMimeType(string extension)
{
return extension.ToLowerInvariant() switch
if (string.IsNullOrWhiteSpace(extension))
{
return "application/octet-stream";
}
var normalized = extension.Trim();
normalized = normalized.StartsWith('.') ? normalized : "." + normalized;
normalized = normalized.ToLowerInvariant();
return normalized switch
{
".txt" => "text/plain",
".pdf" => "application/pdf",
@ -1066,7 +1117,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
// Decode the folderId to get the actual path
var decodedPath = DecodeIdAsPath(folderId);
Logger.LogInformation($"GetFolderPath - FolderId: {folderId}, DecodedPath: {decodedPath}");
// Split path into parts and build breadcrumb
var pathParts = decodedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
@ -1078,8 +1128,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
var pathUpToCurrent = string.Join("/", pathParts.Take(i + 1));
currentEncodedPath = EncodePathAsId(pathUpToCurrent);
Logger.LogInformation($"PathItem {i}: Name='{pathParts[i]}', Id='{currentEncodedPath}', PathUpToCurrent='{pathUpToCurrent}'");
pathItems.Add(new PathItemDto
{
Id = currentEncodedPath,
@ -1087,7 +1135,6 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
});
}
Logger.LogInformation($"Returning {pathItems.Count} breadcrumb items");
return new FolderPathDto { Path = pathItems };
}

View file

@ -11,7 +11,7 @@ using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.GlobalSearchs;
[Authorize(PlatformConsts.AppCodes.Settings.GlobalSearch)]
[Authorize(PlatformConsts.AppCodes.Definitions.GlobalSearch)]
public class GlobalSearchAppService : PlatformAppService
{
private readonly IRepository<GlobalSearch, int> repo;

View file

@ -52,6 +52,7 @@ public class ListFormFieldsAppService : CrudAppService<
entity.FieldName = updateInput.FieldName;
entity.CultureName = updateInput.CultureName;
entity.CaptionName = updateInput.CaptionName;
entity.PlaceHolder = updateInput.PlaceHolder;
entity.BandName = updateInput.BandName;
entity.IsActive = updateInput.IsActive;
entity.Visible = updateInput.Visible;
@ -128,6 +129,7 @@ public class ListFormFieldsAppService : CrudAppService<
{
item.FieldName = input.FieldName;
item.CaptionName = input.CaptionName;
item.PlaceHolder = input.PlaceHolder;
item.BandName = input.BandName;
item.SourceDbType = input.SourceDbType;
item.Alignment = input.Alignment;
@ -251,6 +253,7 @@ public class ListFormFieldsAppService : CrudAppService<
{
field.BandName = sourceField.BandName;
field.CaptionName = sourceField.CaptionName;
field.PlaceHolder = sourceField.PlaceHolder;
field.SourceDbType = sourceField.SourceDbType;
}
if (input.CopiedFields.All || input.CopiedFields.Options)

View file

@ -83,6 +83,7 @@ public class ListFormQueryPreviewAppService : PlatformAppService
{
var authType = op switch
{
OperationEnum.Duplicate => AuthorizationTypeEnum.Create,
OperationEnum.Insert => AuthorizationTypeEnum.Create,
OperationEnum.Update => AuthorizationTypeEnum.Update,
OperationEnum.Delete => AuthorizationTypeEnum.Delete,
@ -104,7 +105,7 @@ public class ListFormQueryPreviewAppService : PlatformAppService
var (_, _, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
return qManager.GenerateQuery(listForm, parameters, op, dataSourceType);
return qManager.GenerateQuery(listForm, listFormFields, parameters, op, dataSourceType);
}
}

View file

@ -6,6 +6,8 @@ using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Queries;
using Microsoft.AspNetCore.Http;
using static Sozsoft.Platform.PlatformConsts;
using System.Collections.Generic;
using System.Text.Json;
namespace Sozsoft.Platform.ListForms.Select;
@ -14,15 +16,18 @@ public class ListFormDataAppService : PlatformAppService
private readonly IListFormAuthorizationManager authManager;
private readonly IQueryManager qManager;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IListFormSelectAppService listFormSelectAppService;
public ListFormDataAppService(
IListFormAuthorizationManager authManager,
IQueryManager qManager,
IHttpContextAccessor httpContextAccessor)
IHttpContextAccessor httpContextAccessor,
IListFormSelectAppService listFormSelectAppService)
{
this.authManager = authManager;
this.qManager = qManager;
this.httpContextAccessor = httpContextAccessor;
this.listFormSelectAppService = listFormSelectAppService;
LocalizationResource = typeof(PlatformResource);
}
@ -39,6 +44,39 @@ public class ListFormDataAppService : PlatformAppService
return await qManager.GenerateAndRunQueryAsync<dynamic>(input.ListFormCode, OperationEnum.Insert, input.Data, queryParameters: queryParameters);
}
public async Task<dynamic> PostDuplicateAsync(DataRequestDto input)
{
// Izin logic process
if (!await authManager.CanAccess(input.ListFormCode, AuthorizationTypeEnum.Create))
throw new Volo.Abp.UserFriendlyException(L[AppErrorCodes.NoAuth]);
var httpContext = httpContextAccessor.HttpContext
?? throw new InvalidOperationException("HTTP Context bulunamadı.");
var queryParameters = httpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
object filter = new object[] { input.Data[0], "=", input.Keys[0] };
var selectRequest = new SelectRequestDto
{
ListFormCode = input.ListFormCode,
Filter = filter.ToString(),
Skip = 0,
Take = 1,
RequireTotalCount = false,
RequireGroupCount = false,
};
var selectResult = await listFormSelectAppService.GetSelectAsync(selectRequest);
var record = ((selectResult?.Data as System.Collections.IEnumerable)?.Cast<object>()?.FirstOrDefault()) ?? throw new Volo.Abp.UserFriendlyException("Kopyalanacak kayıt bulunamadı.");
if (record is not IDictionary<string, object> dict)
throw new Exception("DapperRow IDictionary'e çevrilemedi.");
input.Data = JsonSerializer.Serialize(dict);
return await qManager.GenerateAndRunQueryAsync<dynamic>(input.ListFormCode, OperationEnum.Duplicate, input.Data, null, queryParameters);
}
public async Task<int> PostUpdateAsync(DataRequestDto input)
{
// Izin logic process

View file

@ -306,7 +306,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
};
var queryParameters = httpContextAccessor.HttpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, Enums.OperationEnum.Select, queryParameters: queryParameters);
var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, fields, Enums.OperationEnum.Select, queryParameters: queryParameters);
// Performans: Dictionary ile hızlı lookup
var columnFormatsDict = result.ColumnFormats.ToDictionary(c => c.FieldName, c => c);

View file

@ -289,7 +289,7 @@
{
"code": "App.Sender.Sms.PostaGuvercini.Url",
"nameKey": "App.Sender.Sms.PostaGuvercini.Url",
"descriptionKey": "App.Sender.Sms.PostaGuvercini.Url.Description",
"descriptionKey": "App.Sender.Url.Description",
"defaultValue": "https://www.postaguvercini.com/api_http",
"isVisibleToClients": false,
"providers": "T|G|D",
@ -337,7 +337,7 @@
{
"code": "App.Sender.WhatsApp.Url",
"nameKey": "App.Sender.WhatsApp.Url",
"descriptionKey": "App.Sender.WhatsApp.Url.Description",
"descriptionKey": "App.Sender.Url.Description",
"defaultValue": "https://graph.facebook.com/v21.0",
"isVisibleToClients": false,
"providers": "T|G|D",
@ -401,7 +401,7 @@
{
"code": "App.Sender.Rocket.Url",
"nameKey": "App.Sender.Rocket.Url",
"descriptionKey": "App.Sender.Rocket.Url.Description",
"descriptionKey": "App.Sender.Url.Description",
"defaultValue": "https://chat.sozsoft.com/api/v1",
"isVisibleToClients": false,
"providers": "G|D",

File diff suppressed because it is too large Load diff

View file

@ -903,7 +903,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Email",
CaptionName = "App.Listform.ListformField.Email",
CaptionName = "Abp.Account.EmailAddress",
Width = 300,
ListOrderNo = 2,
Visible = true,
@ -963,7 +963,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "PhoneNumber",
CaptionName = "App.Listform.ListformField.PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 150,
ListOrderNo = 5,
Visible = true,
@ -1016,152 +1016,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
}
#endregion
#region Ip Restriction
listFormName = AppCodes.IdentityManagement.IpRestrictions;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
new ListForm()
{
ListFormType = ListFormTypeEnum.List,
PageSize = 10,
ExportJson = DefaultExportJson,
IsSubForm = false,
ShowNote = true,
LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En,
ListFormCode = listFormName,
Name = listFormName,
Title = listFormName,
DataSourceCode = SeedConsts.DataSources.DefaultCode,
IsTenant = true,
IsBranch = false,
IsOrganizationUnit = false,
Description = listFormName,
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.IpRestriction)),
KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.Guid,
DefaultFilter = DefaultFilterJson,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = DefaultFilterRowJson,
HeaderFilterJson = DefaultHeaderFilterJson,
SearchPanelJson = DefaultSearchPanelJson,
GroupPanelJson = DefaultGroupPanelJson,
SelectionJson = DefaultSelectionSingleJson,
ColumnOptionJson = DefaultColumnOptionJson(),
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = $"UPDATE \"{FullNameTable(TableNameEnum.IpRestriction)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 350, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() {
Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=
[
new EditingFormItemDto { Order = 1, DataField = "ResourceType", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 2, DataField = "ResourceId", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 3, DataField = "IP", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxTextBox },
]}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
}
);
#region Ip Restriction Fields
await _listFormFieldRepository.InsertManyAsync([
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "Id",
CaptionName = "App.Listform.ListformField.Id",
Width = 100,
ListOrderNo = 1,
Visible = false,
IsActive = true,
IsDeleted = false,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "ResourceType",
CaptionName = "App.Listform.ListformField.ResourceType",
Width = 400,
ListOrderNo = 2,
Visible = true,
IsActive = true,
IsDeleted = false,
SortIndex = 1,
SortDirection = GridColumnOptions.SortOrderAsc,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto
{
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key="User", Name="User" },
new () { Key="Role", Name="Role" },
new () { Key="Global", Name="Global" },
}),
}),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "ResourceId",
CaptionName = "App.Listform.ListformField.ResourceId",
Width = 400,
ListOrderNo = 3,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = $"SELECT \"UserName\" AS \"Key\", \"UserName\" AS \"Name\" FROM \"AbpUsers\" UNION SELECT \"Name\" AS \"Key\", \"Name\" AS \"Name\" FROM \"AbpRoles\"",
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "IP",
CaptionName = "App.Listform.ListformField.IP",
Width = 100,
ListOrderNo = 4,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
]);
#endregion
}
#endregion
#region Audit Logs
listFormName = AppCodes.AuditLogs;
listFormName = AppCodes.IdentityManagement.AuditLogs;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -1709,7 +1565,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Sector)),
KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.String,
KeyFieldDbSourceType = DbType.Guid,
DefaultFilter = DefaultFilterJson,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = DefaultFilterRowJson,
@ -1720,9 +1576,9 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
ColumnOptionJson = DefaultColumnOptionJson(),
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Sector)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{
@ -1779,7 +1635,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
#endregion
#region WorkHour
listFormName = AppCodes.Definitions.WorkHour;
listFormName = AppCodes.Restrictions.WorkHour;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -1803,7 +1659,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.WorkHour)),
KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.String,
KeyFieldDbSourceType = DbType.Guid,
DefaultFilter = DefaultFilterJson,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = DefaultFilterRowJson,
@ -1814,8 +1670,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
ColumnOptionJson = DefaultColumnOptionJson(),
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.WorkHour)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 600, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
@ -2034,6 +1890,150 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
#endregion
}
#endregion
#region Ip Restriction
listFormName = AppCodes.Restrictions.IpRestrictions;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
new ListForm()
{
ListFormType = ListFormTypeEnum.List,
PageSize = 10,
ExportJson = DefaultExportJson,
IsSubForm = false,
ShowNote = true,
LayoutJson = DefaultLayoutJson(),
CultureName = LanguageCodes.En,
ListFormCode = listFormName,
Name = listFormName,
Title = listFormName,
DataSourceCode = SeedConsts.DataSources.DefaultCode,
IsTenant = true,
IsBranch = false,
IsOrganizationUnit = false,
Description = listFormName,
SelectCommandType = SelectCommandTypeEnum.Table,
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.IpRestriction)),
KeyFieldName = "Id",
KeyFieldDbSourceType = DbType.Guid,
DefaultFilter = DefaultFilterJson,
SortMode = GridOptions.SortModeSingle,
FilterRowJson = DefaultFilterRowJson,
HeaderFilterJson = DefaultHeaderFilterJson,
SearchPanelJson = DefaultSearchPanelJson,
GroupPanelJson = DefaultGroupPanelJson,
SelectionJson = DefaultSelectionSingleJson,
ColumnOptionJson = DefaultColumnOptionJson(),
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = $"UPDATE \"{FullNameTable(TableNameEnum.IpRestriction)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 350, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() {
Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=
[
new EditingFormItemDto { Order = 1, DataField = "ResourceType", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 2, DataField = "ResourceId", ColSpan = 1, EditorType2=EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 3, DataField = "IP", ColSpan = 1, IsRequired = true, EditorType2=EditorTypes.dxTextBox },
]}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
}
);
#region Ip Restriction Fields
await _listFormFieldRepository.InsertManyAsync([
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.Guid,
FieldName = "Id",
CaptionName = "App.Listform.ListformField.Id",
Width = 100,
ListOrderNo = 1,
Visible = false,
IsActive = true,
IsDeleted = false,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "ResourceType",
CaptionName = "App.Listform.ListformField.ResourceType",
Width = 400,
ListOrderNo = 2,
Visible = true,
IsActive = true,
IsDeleted = false,
SortIndex = 1,
SortDirection = GridColumnOptions.SortOrderAsc,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto
{
DataSourceType = UiLookupDataSourceTypeEnum.StaticData,
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key="User", Name="User" },
new () { Key="Role", Name="Role" },
new () { Key="Global", Name="Global" },
}),
}),
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new() {
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "ResourceId",
CaptionName = "App.Listform.ListformField.ResourceId",
Width = 400,
ListOrderNo = 3,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = $"SELECT \"UserName\" AS \"Key\", \"UserName\" AS \"Name\" FROM \"AbpUsers\" UNION SELECT \"Name\" AS \"Key\", \"Name\" AS \"Name\" FROM \"AbpRoles\"",
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
new()
{
ListFormCode = listForm.ListFormCode,
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "IP",
CaptionName = "App.Listform.ListformField.IP",
Width = 100,
ListOrderNo = 4,
Visible = true,
IsActive = true,
IsDeleted = false,
AllowSearch = true,
ValidationRuleJson = DefaultValidationRuleRequiredJson,
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
},
]);
#endregion
}
#endregion
}
}

View file

@ -15,12 +15,12 @@ public static class ListFormSeeder_DefaultJsons
return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id";
}
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid) => JsonSerializer.Serialize(new FieldsDefaultValue[]
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid, string newId = "@NEWID") => JsonSerializer.Serialize(new FieldsDefaultValue[]
{
new() { FieldName = "CreationTime", FieldDbType = DbType.DateTime, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
new() { FieldName = "IsDeleted", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value },
new() { FieldName = "Id", FieldDbType = dbType, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
new() { FieldName = "Id", FieldDbType = dbType, Value = newId, CustomValueType = FieldCustomValueTypeEnum.CustomKey }
});
public static string DefaultDeleteFieldsDefaultValueJson(DbType dbType = DbType.Guid) => JsonSerializer.Serialize(new FieldsDefaultValue[]

View file

@ -426,7 +426,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Email",
CaptionName = "App.Listform.ListformField.Email",
CaptionName = "Abp.Account.EmailAddress",
Width = 170,
ListOrderNo = 14,
Visible = true,
@ -479,7 +479,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "PhoneNumber",
CaptionName = "App.Listform.ListformField.PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 100,
ListOrderNo = 17,
Visible = true,
@ -684,7 +684,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Code",
CaptionName = "App.Listform.ListformField.Code",
CaptionName = "App.Platform.Code",
Width = 100,
ListOrderNo = 2,
Visible = true,
@ -911,7 +911,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Email",
CaptionName = "App.Listform.ListformField.Email",
CaptionName = "Abp.Account.EmailAddress",
Width = 170,
ListOrderNo = 13,
Visible = true,
@ -964,7 +964,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "PhoneNumber",
CaptionName = "App.Listform.ListformField.PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 100,
ListOrderNo = 16,
Visible = true,
@ -1016,7 +1016,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
#endregion
#region Global Search
listFormName = AppCodes.Settings.GlobalSearch;
listFormName = AppCodes.Definitions.GlobalSearch;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -1060,7 +1060,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items = [
new EditingFormItemDto { Order=1, DataField="System", ColSpan=1, IsRequired=true, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=2, DataField="Group", ColSpan=1, IsRequired=true, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=3, DataField="Term", ColSpan=1, IsRequired=true, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=3, DataField="Term", ColSpan=1, IsRequired=true, EditorType2=EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order=4, DataField="Url", ColSpan=1, IsRequired=true, EditorType2=EditorTypes.dxTextBox },
new EditingFormItemDto { Order=5, DataField="Weight", ColSpan=1, IsRequired=true, EditorType2=EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat() },
]}
@ -1134,6 +1134,12 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
IsActive = true,
IsDeleted = false,
AllowSearch = true,
LookupJson = JsonSerializer.Serialize(new LookupDto {
DataSourceType = UiLookupDataSourceTypeEnum.Query,
DisplayExpr = "Name",
ValueExpr = "Key",
LookupQuery = LookupQueryValues.LanguageKeyValues
}),
ColumnCustomizationJson = DefaultColumnCustomizationJson,
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
PivotSettingsJson = DefaultPivotSettingsJson
@ -1176,7 +1182,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
#endregion
#region AiBot
listFormName = AppCodes.AiBot;
listFormName = AppCodes.Definitions.AiBot;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -1212,7 +1218,10 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PagerOptionJson = DefaultPagerOptionJson,
DeleteCommand = $"DELETE FROM \"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.AiBot))}\" WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Id", FieldDbType = DbType.Int32, Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@ID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}),
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }
}),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 450, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
@ -1472,6 +1481,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Currency)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 350, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -1479,14 +1489,13 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new() {
Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group", Items =
[
new EditingFormItemDto { Order = 1, DataField = "Name", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 2, DataField = "Symbol", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 3, DataField = "Name", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxTextBox },
new EditingFormItemDto { Order = 4, DataField = "Rate", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 5, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox }
new EditingFormItemDto { Order = 3, DataField = "Rate", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxNumberBox },
new EditingFormItemDto { Order = 4, DataField = "IsActive", ColSpan = 1, EditorType2 = EditorTypes.dxCheckBox }
]
}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
});
#region Currency Fields
@ -1617,6 +1626,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.CountryGroup)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -1628,7 +1638,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
]
}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
});
#region CountryGroup Fields
@ -1707,6 +1716,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Country)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -1724,7 +1734,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
]
}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Currency", FieldDbType = DbType.String, Value = "TRY", CustomValueType = FieldCustomValueTypeEnum.Value }
})
@ -2204,6 +2213,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.UomCategory)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, false, false, true),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
@ -2215,7 +2225,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
]
}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
SubFormsJson = JsonSerializer.Serialize(new List<dynamic>() {
new {
TabType = ListFormTabTypeEnum.List,
@ -2310,6 +2319,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
PermissionJson = DefaultPermissionJson(listFormName),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Uom)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
@ -2322,7 +2332,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 5, DataField = "IsActive", ColSpan = 1, EditorType2=EditorTypes.dxCheckBox },
]}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value }
}),
@ -2514,7 +2523,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SkillType)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 400, 200, true, true, true, true, false, true),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
{
@ -2632,7 +2641,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.SkillLevel)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=
@ -2778,7 +2787,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.Skill)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
PagerOptionJson = DefaultPagerOptionJson,
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
EditingOptionJson = DefaultEditingOptionJson(AppCodes.Definitions.SkillLevel, 600, 300, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() { Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[
@ -2848,7 +2857,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
#endregion
#region SettingDefinition
listFormName = AppCodes.Settings.SettingDefinitions;
listFormName = AppCodes.SettingDefinitions;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -2937,7 +2946,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Code",
CaptionName = "App.Listform.ListformField.Code",
CaptionName = "App.Platform.Code",
Width = 400,
ListOrderNo = 4,
Visible = true,
@ -3575,7 +3584,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
#endregion
#region Data Source
listFormName = AppCodes.Listforms.DataSource;
listFormName = AppCodes.DataSource;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -3649,7 +3658,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Code",
CaptionName = "App.Listform.ListformField.Code",
CaptionName = "App.Platform.Code",
Width = 300,
ListOrderNo = 2,
Visible = true,
@ -4881,7 +4890,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
#endregion
#region Route
listFormName = AppCodes.Routes;
listFormName = AppCodes.Menus.Routes;
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
{
var listForm = await _listFormRepository.InsertAsync(
@ -5109,7 +5118,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
}),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.MenuGroup)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
});
#region MenuGroup Fields
@ -5211,7 +5220,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
new EditingFormItemDto { Order = 12, DataField = "ShortName", ColSpan = 1, EditorType2=EditorTypes.dxTextBox },
]}
}),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Code"),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "IsDisabled", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }
})
@ -5242,7 +5251,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Code",
CaptionName = "App.Listform.ListformField.Code",
CaptionName = "App.Platform.Code",
Width = 300,
ListOrderNo = 2,
Visible = true,
@ -5986,7 +5995,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
}),
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.PaymentMethod)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
new() { FieldName = "Commission", FieldDbType = DbType.Decimal, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value }
}),
@ -6499,7 +6508,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "PhoneNumber",
CaptionName = "App.Listform.ListformField.PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 100,
ListOrderNo = 11,
Visible = true,
@ -7366,7 +7375,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "Email",
CaptionName = "App.Listform.ListformField.Email",
CaptionName = "Abp.Account.EmailAddress",
Width = 250,
ListOrderNo = 4,
Visible = true,
@ -7384,7 +7393,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
CultureName = LanguageCodes.En,
SourceDbType = DbType.String,
FieldName = "PhoneNumber",
CaptionName = "App.Listform.ListformField.PhoneNumber",
CaptionName = "Abp.Identity.User.UserInformation.PhoneNumber",
Width = 100,
ListOrderNo = 5,
Visible = true,

View file

@ -21,13 +21,6 @@
"routeType": "public",
"authority": []
},
{
"key": "about",
"path": "/about",
"componentPath": "@/views/public/About",
"routeType": "public",
"authority": []
},
{
"key": "products",
"path": "/products",
@ -187,7 +180,7 @@
"path": "/admin/ai",
"componentPath": "@/views/ai/Assistant",
"routeType": "protected",
"authority": ["App.AiBot.Asistant"]
"authority": ["App.Definitions.AiBot.Asistant"]
},
{
"key": "admin.profile.general",
@ -452,22 +445,22 @@
},
{
"ParentCode": "App.Saas.Definitions",
"Code": "App.AiBot",
"DisplayName": "App.AiBot",
"Code": "App.Definitions.AiBot",
"DisplayName": "App.Definitions.AiBot",
"Order": 1,
"Url": "/admin/list/App.AiBot",
"Url": "/admin/list/App.Definitions.AiBot",
"Icon": "FcMindMap",
"RequiredPermissionName": "App.AiBot",
"RequiredPermissionName": "App.Definitions.AiBot",
"IsDisabled": false
},
{
"ParentCode": "App.Saas.Definitions",
"Code": "App.Settings.GlobalSearch",
"DisplayName": "App.Settings.GlobalSearch",
"Code": "App.Definitions.GlobalSearch",
"DisplayName": "App.Definitions.GlobalSearch",
"Order": 2,
"Url": "/admin/list/App.Settings.GlobalSearch",
"Url": "/admin/list/App.Definitions.GlobalSearch",
"Icon": "FcSearch",
"RequiredPermissionName": "App.Settings.GlobalSearch",
"RequiredPermissionName": "App.Definitions.GlobalSearch",
"IsDisabled": false
},
{
@ -552,12 +545,12 @@
},
{
"ParentCode": "App.Saas",
"Code": "App.Settings.SettingDefinitions",
"DisplayName": "App.Settings.SettingDefinitions",
"Code": "App.SettingDefinitions",
"DisplayName": "App.SettingDefinitions",
"Order": 5,
"Url": "/admin/list/App.Settings.SettingDefinitions",
"Url": "/admin/list/App.SettingDefinitions",
"Icon": "FcSupport",
"RequiredPermissionName": "App.Settings.SettingDefinitions",
"RequiredPermissionName": "App.SettingDefinitions",
"IsDisabled": false
},
{
@ -592,12 +585,12 @@
},
{
"ParentCode": "App.Saas",
"Code": "App.Listforms.DataSource",
"DisplayName": "App.Listforms.DataSource",
"Code": "App.DataSource",
"DisplayName": "App.DataSource",
"Order": 7,
"Url": "/admin/list/App.Listforms.DataSource",
"Url": "/admin/list/App.DataSource",
"Icon": "FcAcceptDatabase",
"RequiredPermissionName": "App.Listforms.DataSource",
"RequiredPermissionName": "App.DataSource",
"IsDisabled": false
},
{
@ -784,12 +777,12 @@
},
{
"ParentCode": "App.Menus",
"Code": "App.Routes",
"DisplayName": "App.Routes",
"Code": "App.Menus.Routes",
"DisplayName": "App.Menus.Routes",
"Order": 1,
"Url": "/admin/list/App.Routes",
"Url": "/admin/list/App.Menus.Routes",
"Icon": "FaSynagogue",
"RequiredPermissionName": "App.Routes",
"RequiredPermissionName": "App.Menus.Routes",
"IsDisabled": false
},
{
@ -822,16 +815,6 @@
"RequiredPermissionName": "App.Menus.Manager",
"IsDisabled": false
},
{
"ParentCode": "App.Saas",
"Code": "App.Files",
"DisplayName": "App.Files",
"Order": 14,
"Url": "/admin/files",
"Icon": "FcFolder",
"RequiredPermissionName": "App.Files",
"IsDisabled": false
},
{
"ParentCode": "App.Saas",
"Code": "App.DeveloperKit",
@ -955,20 +938,40 @@
"IsDisabled": false
},
{
"ParentCode": "App.Administration.Definitions",
"Code": "App.Definitions.WorkHour",
"DisplayName": "App.Definitions.WorkHour",
"Order": 2,
"Url": "/admin/list/App.Definitions.WorkHour",
"ParentCode": "App.Administration",
"Code": "App.Administration.Restrictions",
"DisplayName": "App.Restrictions",
"Order": 3,
"Url": null,
"Icon": "FaLock",
"RequiredPermissionName": null,
"IsDisabled": false
},
{
"ParentCode": "App.Administration.Restrictions",
"Code": "App.Restrictions.WorkHour",
"DisplayName": "App.Restrictions.WorkHour",
"Order": 1,
"Url": "/admin/list/App.Restrictions.WorkHour",
"Icon": "FcClock",
"RequiredPermissionName": "App.Definitions.WorkHour",
"RequiredPermissionName": "App.Restrictions.WorkHour",
"IsDisabled": false
},
{
"ParentCode": "App.Administration.Restrictions",
"Code": "App.Restrictions.IpRestrictions",
"DisplayName": "App.Restrictions.IpRestrictions",
"Order": 2,
"Url": "/admin/list/App.Restrictions.IpRestrictions",
"Icon": "FcNfcSign",
"RequiredPermissionName": "App.Restrictions.IpRestrictions",
"IsDisabled": false
},
{
"ParentCode": "App.Administration",
"Code": "Abp.Identity",
"DisplayName": "Abp.Identity",
"Order": 3,
"Order": 4,
"Url": null,
"Icon": "FcConferenceCall",
"RequiredPermissionName": null,
@ -1036,29 +1039,19 @@
},
{
"ParentCode": "Abp.Identity",
"Code": "App.IpRestrictions",
"DisplayName": "App.IpRestrictions",
"Code": "App.IdentityManagement.AuditLogs",
"DisplayName": "App.IdentityManagement.AuditLogs",
"Order": 7,
"Url": "/admin/list/App.IpRestrictions",
"Icon": "FcNfcSign",
"RequiredPermissionName": "App.IpRestrictions",
"IsDisabled": false
},
{
"ParentCode": "Abp.Identity",
"Code": "App.AuditLogs",
"DisplayName": "App.AuditLogs",
"Order": 8,
"Url": "/admin/list/App.AuditLogs",
"Url": "/admin/list/App.IdentityManagement.AuditLogs",
"Icon": "FcMultipleInputs",
"RequiredPermissionName": "App.AuditLogs",
"RequiredPermissionName": "App.IdentityManagement.AuditLogs",
"IsDisabled": false
},
{
"ParentCode": "App.Administration",
"Code": "App.Reports.Management",
"DisplayName": "App.Reports.Management",
"Order": 4,
"Order": 5,
"Url": null,
"Icon": "FcDocument",
"RequiredPermissionName": null,
@ -1084,11 +1077,21 @@
"RequiredPermissionName": "App.Reports.ReportTemplates",
"IsDisabled": false
},
{
"ParentCode": "App.Administration",
"Code": "App.Files",
"DisplayName": "App.Files",
"Order": 6,
"Url": "/admin/files",
"Icon": "FcFolder",
"RequiredPermissionName": "App.Files",
"IsDisabled": false
},
{
"ParentCode": "App.Administration",
"Code": "App.Forum",
"DisplayName": "App.Forum",
"Order": 5,
"Order": 7,
"Url": "/admin/forum",
"Icon": "FcLink",
"RequiredPermissionName": "App.ForumManagement.Publish",

View file

@ -11,6 +11,7 @@ public enum OperationEnum
Delete,
DeleteBefore,
DeleteAfter,
Select
Select,
Duplicate,
}

View file

@ -290,42 +290,80 @@ public static class PlatformConsts
public static class AppCodes
{
public const string Home = Prefix.App + ".Home";
public const string Saas = Prefix.App + ".Saas";
public const string Branches = Prefix.App + ".Branches";
public static class Settings
public static class Definitions
{
public const string Default = Prefix.App + ".Settings";
public const string Default = Prefix.App + ".Definitions";
public const string AiBot = Default + ".AiBot";
public const string GlobalSearch = Default + ".GlobalSearch";
public const string SettingDefinitions = Default + ".SettingDefinitions";
public const string ContactTitle = Default + ".ContactTitle";
public const string Currency = Default + ".Currency";
public const string CountryGroup = Default + ".CountryGroup";
public const string Country = Default + ".Country";
public const string City = Default + ".City";
public const string District = Default + ".District";
public const string SkillType = Default + ".SkillType";
public const string UomCategory = Default + ".UomCategory";
public const string Sector = Default + ".Sector";
}
public static class Restrictions
{
public const string Default = Prefix.App + ".Restrictions";
public const string WorkHour = Default + ".WorkHour";
public const string IpRestrictions = Default + ".IpRestrictions";
}
public const string SettingDefinitions = Prefix.App + ".SettingDefinitions";
public static class Languages
{
public const string Default = Prefix.App + ".Languages";
public const string Language = Default + ".Language";
public const string LanguageText = Default + ".LanguageText";
}
public const string Menus = Prefix.App + ".Menus";
public const string DataSource = Prefix.App + ".DataSource";
public static class Listforms
{
public const string Default = Prefix.App + ".Listforms";
public const string Wizard = Default + ".Wizard";
public const string DataSource = Default + ".DataSource";
public const string Listform = Default + ".Listform";
public const string ListformField = Default + ".ListformField";
public const string Chart = Default + ".Chart";
}
public static class Notifications
{
public const string Default = Prefix.App + ".Notifications";
public const string NotificationRules = Default + ".NotificationRules";
public const string Notification = Default + ".Notification";
}
public const string BackgroundWorkers = Prefix.App + ".BackgroundWorkers";
public static class Menus
{
public const string Default = Prefix.App + ".Menus";
public const string Routes = Default + ".Routes";
public const string MenuGroup = Default + ".MenuGroup";
public const string Menu = Default + ".Menu";
public const string Manager = Default + ".Manager";
}
public static class DeveloperKits
{
public const string Default = Prefix.App + ".DeveloperKit";
public const string CustomEndpoints = Default + ".CustomEndpoints";
public const string Get = CustomEndpoints + ".Get";
@ -346,63 +384,48 @@ public static class PlatformConsts
public const string ViewCode = DynamicService + ".ViewCode";
}
}
public const string Blog = Prefix.App + ".Blog";
public const string Forum = Prefix.App + ".Forum";
//Web Site
public const string Home = Prefix.App + ".Home";
public const string About = Prefix.App + ".About";
public const string Services = Prefix.App + ".Services";
public static class Orders
{
public const string Default = Prefix.App + ".Orders";
public const string Products = Default + ".Products";
public const string PaymentMethods = Default + ".PaymentMethods";
public const string InstallmentOptions = Default + ".InstallmentOptions";
public const string SalesOrders = Default + ".SalesOrders";
}
public static class BlogManagement
{
public const string Default = Prefix.App + ".BlogManagement";
public const string BlogPosts = Default + ".Posts";
public const string BlogCategory = Default + ".Category";
}
public const string Demos = Prefix.App + ".Demos";
public const string Contact = Prefix.App + ".Contact";
//Administration
public const string Administration = Prefix.App + ".Administration";
public const string Setting = Prefix.App + ".Setting";
public static class IdentityManagement
{
public const string ClaimTypes = Prefix.App + ".ClaimType";
public const string IpRestrictions = Prefix.App + ".IpRestrictions";
public const string Default = Prefix.App + ".IdentityManagement";
public const string ClaimTypes = Default + ".ClaimType";
public const string AuditLogs = Default + ".AuditLogs";
}
public const string AuditLogs = Prefix.App + ".AuditLogs";
public static class Definitions
public static class Reports
{
public const string ContactTag = Default + ".ContactTag";
public const string ContactTitle = Default + ".ContactTitle";
public const string Currency = Default + ".Currency";
public const string CountryGroup = Default + ".CountryGroup";
public const string Country = Default + ".Country";
public const string City = Default + ".City";
public const string District = Default + ".District";
public const string Default = Prefix.App + ".Definitions";
public const string Sector = Default + ".Sector";
public const string SkillType = Default + ".SkillType";
public const string UomCategory = Default + ".UomCategory";
public const string Bank = Default + ".Bank";
public const string Behavior = Default + ".Behavior";
public const string Disease = Default + ".Disease";
public const string Document = Default + ".Document";
public const string EducationStatus = Default + ".EducationStatus";
public const string MeetingMethod = Default + ".MeetingMethod";
public const string MeetingResult = Default + ".MeetingResult";
public const string Program = Default + ".Program";
public const string Interesting = Default + ".Interesting";
public const string SalesRejectionReason = Default + ".SalesRejectionReason";
public const string ClassCancellationReason = Default + ".ClassCancellationReason";
public const string Source = Default + ".Source";
public const string Vaccine = Default + ".Vaccine";
public const string NoteType = Default + ".NoteType";
public const string WorkHour = Default + ".WorkHour";
public const string Vehicle = Default + ".Vehicle";
public const string Schedule = Default + ".Schedule";
public const string ScheduleLesson = Default + ".ScheduleLesson";
public const string Psychologist = Default + ".Psychologist";
public const string Meal = Default + ".Meal";
public const string Lawyer = Default + ".Lawyer";
public const string LessonPeriod = Default + ".LessonPeriod";
public const string RegistrationType = Default + ".RegistrationType";
public const string RegistrationMethod = Default + ".RegistrationMethod";
public const string ClassType = Default + ".ClassType";
public const string Class = Default + ".Class";
public const string Level = Default + ".Level";
}
public static class Hr
{
public const string Default = Prefix.App + ".Hr";
public const string EventType = Default + ".EventType";
public const string EventCategory = Default + ".EventCategory";
public const string Event = Default + ".Event";
public const string Default = Prefix.App + ".Reports";
public const string Categories = Default + ".Categories";
public const string ReportTemplates = Default + ".ReportTemplates";
}
}

View file

@ -318,70 +318,77 @@ public static class SeedConsts
public static class AppCodes
{
public const string Home = Prefix.App + ".Home";
//Saas
public const string Saas = Prefix.App + ".Saas";
public const string Branches = Prefix.App + ".Branches";
public static class Settings
public static class Definitions
{
public const string Default = Prefix.App + ".Settings";
public const string Default = Prefix.App + ".Definitions";
public const string AiBot = Default + ".AiBot";
public const string GlobalSearch = Default + ".GlobalSearch";
public const string SettingDefinitions = Default + ".SettingDefinitions";
public const string ContactTitle = Default + ".ContactTitle";
public const string Currency = Default + ".Currency";
public const string CountryGroup = Default + ".CountryGroup";
public const string Country = Default + ".Country";
public const string City = Default + ".City";
public const string District = Default + ".District";
public const string SkillType = Default + ".SkillType";
public const string SkillLevel = Default + ".SkillLevel";
public const string Skill = Default + ".Skill";
public const string UomCategory = Default + ".UomCategory";
public const string Uom = Default + ".Uom";
public const string Sector = Default + ".Sector";
}
public const string AiBot = Prefix.App + ".AiBot";
public static class Restrictions
{
public const string Default = Prefix.App + ".Restrictions";
public const string WorkHour = Default + ".WorkHour";
public const string IpRestrictions = Default + ".IpRestrictions";
}
public const string SettingDefinitions = Prefix.App + ".SettingDefinitions";
public static class Languages
{
public const string Default = Prefix.App + ".Languages";
public const string Language = Default + ".Language";
public const string LanguageText = Default + ".LanguageText";
}
public const string Routes = Prefix.App + ".Routes";
public static class Menus
{
public const string Default = Prefix.App + ".Menus";
public const string MenuGroup = Default + ".MenuGroup";
public const string Menu = Default + ".Menu";
public const string Manager = Default + ".Manager";
}
public const string DataSource = Prefix.App + ".DataSource";
public static class Listforms
{
public const string Default = Prefix.App + ".Listforms";
public const string DataSource = Default + ".DataSource";
public const string Wizard = Default + ".Wizard";
public const string Listform = Default + ".Listform";
public const string ListformField = Default + ".ListformField";
public const string Chart = Default + ".Chart";
}
public static class Notifications
{
public const string Default = Prefix.App + ".Notifications";
public const string NotificationRules = Default + ".NotificationRules";
public const string Notification = Default + ".Notification";
}
public const string BackgroundWorkers = Prefix.App + ".BackgroundWorkers";
public const string Forum = Prefix.App + ".Forum";
public static class DeveloperKits
{
public const string Default = Prefix.App + ".DeveloperKit";
public const string CustomEndpoints = Default + ".CustomEndpoints";
}
public static class Reports
{
public const string Default = Prefix.App + ".Reports";
public const string Categories = Default + ".Categories";
public const string ReportTemplates = Default + ".ReportTemplates";
}
//Web Site
public const string Home = Prefix.App + ".Home";
public const string About = Prefix.App + ".About";
public const string Services = Prefix.App + ".Services";
public static class Orders
{
public const string Default = Prefix.App + ".Orders";
public const string Products = Default + ".Products";
public const string PaymentMethods = Default + ".PaymentMethods";
public const string InstallmentOptions = Default + ".InstallmentOptions";
@ -397,32 +404,43 @@ public static class SeedConsts
public const string Demos = Prefix.App + ".Demos";
public const string Contact = Prefix.App + ".Contact";
public static class Menus
{
public const string Default = Prefix.App + ".Menus";
public const string Routes = Default + ".Routes";
public const string MenuGroup = Default + ".MenuGroup";
public const string Menu = Default + ".Menu";
public const string Manager = Default + ".Manager";
}
public static class DeveloperKits
{
public const string Default = Prefix.App + ".DeveloperKit";
public const string CustomEndpoints = Default + ".CustomEndpoints";
}
public const string Forum = Prefix.App + ".Forum";
//Administration
public const string Administration = Prefix.App + ".Administration";
public const string Setting = Prefix.App + ".Setting";
public static class IdentityManagement
{
public const string ClaimTypes = Prefix.App + ".ClaimType";
public const string IpRestrictions = Prefix.App + ".IpRestrictions";
}
public const string AuditLogs = Prefix.App + ".AuditLogs";
public static class Definitions
{
public const string Default = Prefix.App + ".Definitions";
public const string Default = Prefix.App + ".IdentityManagement";
public const string ContactTitle = Default + ".ContactTitle";
public const string Currency = Default + ".Currency";
public const string CountryGroup = Default + ".CountryGroup";
public const string Country = Default + ".Country";
public const string City = Default + ".City";
public const string District = Default + ".District";
public const string Sector = Default + ".Sector";
public const string SkillType = Default + ".SkillType";
public const string SkillLevel = Default + ".SkillLevel";
public const string Skill = Default + ".Skill";
public const string UomCategory = Default + ".UomCategory";
public const string Uom = Default + ".Uom";
public const string WorkHour = Default + ".WorkHour";
public const string ClaimTypes = Default + ".ClaimType";
public const string AuditLogs = Default + ".AuditLogs";
}
public static class Reports
{
public const string Default = Prefix.App + ".Reports";
public const string Categories = Default + ".Categories";
public const string ReportTemplates = Default + ".ReportTemplates";
}
}

View file

@ -13,6 +13,7 @@ public class ListFormField : FullAuditedEntity<Guid>
public string CultureName { get; set; } // Bu tanım hangi dil için (“tr”, “en”)
public string FieldName { get; set; } // Kaynaktaki sutun adi
public string CaptionName { get; set; } // Sutun basligi
public string PlaceHolder { get; set; } // Sutun placeholder'i
public bool? Visible { get; set; } // Liste üzerinde gösterilecek mi? Yoksa eklenebilir sütunların arasında mı duracak. select sorgusuna dahildir
public bool? IsActive { get; set; } = true; // Sadece IsActive olan alanlar sorguya dahil edilir
public int? Width { get; set; } // Sütunun listedeki genişliği

View file

@ -7,7 +7,6 @@ using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text.Json;
@ -95,6 +94,7 @@ public class ListFormManager : PlatformDomainService, IListFormManager
var field = listFormFields.FirstOrDefault(c => c.FieldName == item.Key);
if (field == null
|| (op == OperationEnum.Insert && !field.CanCreate)
|| (op == OperationEnum.Duplicate && !field.CanCreate)
|| (op == OperationEnum.Update && !field.CanUpdate)
)
{
@ -106,7 +106,7 @@ public class ListFormManager : PlatformDomainService, IListFormManager
}
// ListFormField icerisinde olmayan (select sorgusu ile alinmayan) deger almasi gereken zorunlu alanlar
var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, op, keys, queryParameters);
var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, listFormFields, op, keys, queryParameters, inputParams);
if (PlatformConsts.IsMultiTenant && listForm.IsTenant)
{

View file

@ -20,9 +20,11 @@ public interface IDefaultValueManager
{
Task<Dictionary<string, object>> GenerateDefaultValuesAsync(
ListForm listForm,
List<ListFormField> listFormFields,
OperationEnum op,
object[] keys = null,
Dictionary<string, StringValues> queryParameters = null);
Dictionary<string, StringValues> queryParameters = null,
dynamic inputParams = null);
}
public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
@ -39,15 +41,18 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
public async Task<Dictionary<string, object>> GenerateDefaultValuesAsync(
ListForm listForm,
List<ListFormField> listFormFields,
OperationEnum op,
object[] keys = null,
Dictionary<string, StringValues> queryParameters = null)
Dictionary<string, StringValues> queryParameters = null,
dynamic inputParams = null)
{
var fields = new Dictionary<string, object>();
var defaultFieldsJson = op switch
{
OperationEnum.Insert => listForm.InsertFieldsDefaultValueJson,
OperationEnum.Duplicate => listForm.InsertFieldsDefaultValueJson,
OperationEnum.Update => listForm.UpdateFieldsDefaultValueJson,
OperationEnum.Delete => listForm.DeleteFieldsDefaultValueJson,
OperationEnum.Select => listForm.FormFieldsDefaultValueJson,
@ -60,69 +65,88 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
return fields;
}
foreach (var field in JsonSerializer.Deserialize<List<FieldsDefaultValue>>(defaultFieldsJson))
foreach (var defaultField in JsonSerializer.Deserialize<List<FieldsDefaultValue>>(defaultFieldsJson))
{
object value = null;
switch (field.CustomValueType)
switch (defaultField.CustomValueType)
{
case FieldCustomValueTypeEnum.CustomKey:
if (field.Value == PlatformConsts.DefaultValues.UserId)
if (defaultField.Value == PlatformConsts.DefaultValues.UserId)
value = CurrentUser.Id;
else if (field.Value == PlatformConsts.DefaultValues.UserName)
else if (defaultField.Value == PlatformConsts.DefaultValues.UserName)
value = CurrentUser.Name;
else if (field.Value == PlatformConsts.DefaultValues.Roles)
else if (defaultField.Value == PlatformConsts.DefaultValues.Roles)
value = CurrentUser.Roles; //.JoinAsString("','");
else if (field.Value == PlatformConsts.DefaultValues.Date)
else if (defaultField.Value == PlatformConsts.DefaultValues.Date)
value = Clock.Now.Date;
else if (field.Value == PlatformConsts.DefaultValues.Now)
else if (defaultField.Value == PlatformConsts.DefaultValues.Now)
value = Clock.Now;
else if (field.Value == PlatformConsts.DefaultValues.Day)
else if (defaultField.Value == PlatformConsts.DefaultValues.Day)
value = Clock.Now.Day;
else if (field.Value == PlatformConsts.DefaultValues.Month)
else if (defaultField.Value == PlatformConsts.DefaultValues.Month)
value = Clock.Now.Month;
else if (field.Value == PlatformConsts.DefaultValues.Year)
else if (defaultField.Value == PlatformConsts.DefaultValues.Year)
value = Clock.Now.Year;
else if (field.Value == PlatformConsts.DefaultValues.Id)
else if (defaultField.Value == PlatformConsts.DefaultValues.Id)
value = keys?.FirstOrDefault();
else if (field.Value == PlatformConsts.DefaultValues.NewId)
else if (defaultField.Value == PlatformConsts.DefaultValues.NewId)
value = Guid.NewGuid();
else if (field.Value == PlatformConsts.DefaultValues.Selected_Ids)
else if (defaultField.Value == PlatformConsts.DefaultValues.Selected_Ids)
value = keys;
else if (field.Value == PlatformConsts.DefaultValues.TenantId)
else if (defaultField.Value == PlatformConsts.DefaultValues.TenantId)
value = CurrentTenant.Id;
else
value = field.Value;
{
if ((object)inputParams != null)
{
foreach (var item in JsonSerializer.Deserialize<Dictionary<string, object>>(inputParams))
{
var field = listFormFields.FirstOrDefault(c => c.FieldName == item.Key);
if (field == null
|| (op == OperationEnum.Insert && !field.CanCreate)
|| (op == OperationEnum.Duplicate && !field.CanCreate)
|| (op == OperationEnum.Update && !field.CanUpdate)
)
{
continue;
}
value = QueryHelper.GetFormattedValue(field.SourceDbType, item.Value);
}
}
}
//value = field.Value;
//TODO: artirilabilir
break;
case FieldCustomValueTypeEnum.QueryParams:
if (queryParameters != null)
{
value = queryParameters.GetValueOrDefault(field.Value);
value = queryParameters.GetValueOrDefault(defaultField.Value);
}
break;
case FieldCustomValueTypeEnum.DbQuery:
if (!string.IsNullOrWhiteSpace(field.SqlQuery))
if (!string.IsNullOrWhiteSpace(defaultField.SqlQuery))
{
var dynamicDataManager = LazyServiceProvider.LazyGetRequiredService<IDynamicDataManager>();
var (dynamicDataRepository, connectionString, _) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
value = await dynamicDataRepository.ExecuteScalarAsync<object>(field.SqlQuery, connectionString);
value = await dynamicDataRepository.ExecuteScalarAsync<object>(defaultField.SqlQuery, connectionString);
}
break;
case FieldCustomValueTypeEnum.Value:
default:
value = field.Value;
value = defaultField.Value;
break;
}
if (value != null && !string.IsNullOrWhiteSpace(value.ToString()))
{
var formattedValue = QueryHelper.GetFormattedValue(field.FieldDbType, value);
if (fields.ContainsKey(field.FieldName))
var formattedValue = QueryHelper.GetFormattedValue(defaultField.FieldDbType, value);
if (fields.ContainsKey(defaultField.FieldName))
{
fields[field.FieldName] = formattedValue;
fields[defaultField.FieldName] = formattedValue;
}
else
{
fields.Add(field.FieldName, formattedValue);
fields.Add(defaultField.FieldName, formattedValue);
}
}
}

View file

@ -27,6 +27,7 @@ public interface IQueryManager
string GenerateQuery(
ListForm listForm,
List<ListFormField> listFormFields,
Dictionary<string, object> parameters,
OperationEnum op,
DataSourceTypeEnum dataSourceType,
@ -77,24 +78,20 @@ public class QueryManager : PlatformDomainService, IQueryManager
var listFormFields = await listFormFieldManager.GetUserListFormFields(listFormCode);
var parameters = await listFormManager.GetParametersAsync(listForm, listFormFields, inputParams, op, keys, queryParameters);
// if (parameters == null || parameters.Count == 0)
// {
// throw new UserFriendlyException(localizer[AppErrorCodes.ParameterNotValid]);
// }
var (dynamicDataRepository, connectionString, dataSourceType) = await dynamicDataManager.GetAsync(listForm.IsTenant, listForm.DataSourceCode);
var sql = GenerateQuery(listForm, parameters, op, dataSourceType, keys);
var sql = GenerateQuery(listForm, listFormFields, parameters, op, dataSourceType, keys);
// Sorguyu calistir
if (!string.IsNullOrEmpty(sql))
{
// TODO: Log
if (op == OperationEnum.Insert)
if (op == OperationEnum.Insert || op == OperationEnum.Duplicate)
{
if (!string.IsNullOrEmpty(listForm.InsertBeforeCommand))
{
var beforeSql = GenerateQuery(listForm, parameters, OperationEnum.InsertBefore, dataSourceType, keys);
var beforeSql = GenerateQuery(listForm, listFormFields, parameters, OperationEnum.InsertBefore, dataSourceType, keys);
await dynamicDataRepository.ExecuteAsync(beforeSql, connectionString, parameters);
}
@ -102,7 +99,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
if (!string.IsNullOrEmpty(listForm.InsertAfterCommand))
{
var afterSql = GenerateQuery(listForm, parameters, OperationEnum.InsertAfter, dataSourceType, keys);
var afterSql = GenerateQuery(listForm, listFormFields, parameters, OperationEnum.InsertAfter, dataSourceType, keys);
await dynamicDataRepository.ExecuteAsync(afterSql, connectionString, parameters);
}
@ -113,12 +110,12 @@ public class QueryManager : PlatformDomainService, IQueryManager
// Before komutlari varsa calistir
if (op == OperationEnum.Update && !string.IsNullOrEmpty(listForm.UpdateBeforeCommand))
{
var beforeSql = GenerateQuery(listForm, parameters, OperationEnum.UpdateBefore, dataSourceType, keys);
var beforeSql = GenerateQuery(listForm, listFormFields, parameters, OperationEnum.UpdateBefore, dataSourceType, keys);
await dynamicDataRepository.ExecuteAsync(beforeSql, connectionString, parameters);
}
else if (op == OperationEnum.Delete && !string.IsNullOrEmpty(listForm.DeleteBeforeCommand))
{
var beforeSql = GenerateQuery(listForm, parameters, OperationEnum.DeleteBefore, dataSourceType, keys);
var beforeSql = GenerateQuery(listForm, listFormFields, parameters, OperationEnum.DeleteBefore, dataSourceType, keys);
await dynamicDataRepository.ExecuteAsync(beforeSql, connectionString, parameters);
}
@ -128,12 +125,12 @@ public class QueryManager : PlatformDomainService, IQueryManager
// After komutlari varsa calistir
if (op == OperationEnum.Update && !string.IsNullOrEmpty(listForm.UpdateAfterCommand))
{
var afterSql = GenerateQuery(listForm, parameters, OperationEnum.UpdateAfter, dataSourceType, keys);
var afterSql = GenerateQuery(listForm, listFormFields, parameters, OperationEnum.UpdateAfter, dataSourceType, keys);
await dynamicDataRepository.ExecuteAsync(afterSql, connectionString, parameters);
}
else if (op == OperationEnum.Delete && !string.IsNullOrEmpty(listForm.DeleteAfterCommand))
{
var afterSql = GenerateQuery(listForm, parameters, OperationEnum.DeleteAfter, dataSourceType, keys);
var afterSql = GenerateQuery(listForm, listFormFields, parameters, OperationEnum.DeleteAfter, dataSourceType, keys);
await dynamicDataRepository.ExecuteAsync(afterSql, connectionString, parameters);
}
@ -146,6 +143,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
public string GenerateQuery(
ListForm listForm,
List<ListFormField> listFormField,
Dictionary<string, object> parameters,
OperationEnum op,
DataSourceTypeEnum dataSourceType,
@ -153,6 +151,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
{
var command = op switch
{
OperationEnum.Duplicate => listForm.InsertCommand,
OperationEnum.Insert => listForm.InsertCommand,
OperationEnum.InsertBefore => listForm.InsertBeforeCommand,
OperationEnum.InsertAfter => listForm.InsertAfterCommand,
@ -177,7 +176,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
var fieldString = string.Join(',', parameters.Keys.Select(a => $"\"{a}\"").ToList());
var fieldParams = string.Join(',', parameters.Keys.Select(a => $"@{a}"));
if (op == OperationEnum.Insert)
if (op == OperationEnum.Insert || op == OperationEnum.Duplicate)
{
sql = dataSourceType switch
{

View file

@ -365,6 +365,7 @@ public class PlatformDbContext :
b.Property(a => a.CultureName).HasMaxLength(10).IsRequired();
b.Property(a => a.FieldName).IsRequired().HasMaxLength(128);
b.Property(a => a.CaptionName).HasMaxLength(256);
b.Property(a => a.PlaceHolder).HasMaxLength(256);
// Varsayılan değerler
b.Property(a => a.AllowSearch).HasDefaultValue(false);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260317181749_Initial")]
[Migration("20260330120142_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -2656,6 +2656,10 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("PivotSettingsJson")
.HasColumnType("text");
b.Property<string>("PlaceHolder")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("RoleId")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");

View file

@ -2031,6 +2031,7 @@ namespace Sozsoft.Platform.Migrations
CultureName = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
FieldName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
CaptionName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
PlaceHolder = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Visible = table.Column<bool>(type: "bit", nullable: true, defaultValue: true),
IsActive = table.Column<bool>(type: "bit", nullable: true, defaultValue: true),
Width = table.Column<int>(type: "int", nullable: true, defaultValue: 100),

View file

@ -2653,6 +2653,10 @@ namespace Sozsoft.Platform.Migrations
b.Property<string>("PivotSettingsJson")
.HasColumnType("text");
b.Property<string>("PlaceHolder")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("RoleId")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");

View file

@ -4,6 +4,7 @@ using DevExpress.DataAccess.Sql;
using DevExpress.XtraReports.Web.ReportDesigner;
using DevExpress.XtraReports.Web.ReportDesigner.Services;
using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform.Enums;
namespace Sozsoft.Platform.Controllers;
@ -23,9 +24,9 @@ public class CustomReportDesignerController : ReportDesignerController
var ds = new SqlDataSource("SqlServer");
SelectQuery query = SelectQueryFluentBuilder
.AddTable("Sas_T_Sector")
.AddTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Sector)))
.SelectAllColumnsFromTable()
.Build("Sas_T_Sector");
.Build(TableNameResolver.GetFullTableName(nameof(TableNameEnum.Sector)));
ds.Queries.Add(query);
ds.RebuildResultSchema();

View file

@ -0,0 +1,281 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform;
/// <summary>
/// Veritabanı henüz hazır değilken çalışan minimal kurulum uygulaması.
/// Tam ABP stack yüklemez; sadece /api/setup/* endpointlerini sunar.
/// </summary>
internal static class SetupAppRunner
{
// Veritabanı Hazırlık Kontrolü
/// <summary>
/// DB var mı ve AbpRoles tablosu oluşmuş mu diye kontrol eder.
/// Boş DB veya bağlantı hatası durumunda false döner.
/// </summary>
public static bool DatabaseIsReady(IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider);
if (string.IsNullOrWhiteSpace(connectionString))
return false;
try
{
if (DefaultDatabaseProvider == DatabaseProvider.SqlServer)
return SqlServerIsReady(connectionString);
#pragma warning disable CS0162
return true; // Diğer sağlayıcılar için geçici — ileride PostgreSQL desteği eklenecek
#pragma warning restore CS0162
}
catch (Exception ex)
{
Log.Warning("Veritabanı hazırlık kontrolü başarısız: {Error}", ex.Message);
return false;
}
}
private static bool SqlServerIsReady(string connectionString)
{
var csb = new SqlConnectionStringBuilder(connectionString);
var dbName = csb.InitialCatalog;
if (string.IsNullOrEmpty(dbName))
return false;
// 1) master'a bağlan — DB varlığını kontrol et
var masterCsb = new SqlConnectionStringBuilder(connectionString)
{
InitialCatalog = "master",
ConnectTimeout = 8
};
using var masterConn = new SqlConnection(masterCsb.ConnectionString);
masterConn.Open();
using var dbCheck = new SqlCommand(
"SELECT COUNT(1) FROM sys.databases WHERE name = @n", masterConn);
dbCheck.Parameters.AddWithValue("@n", dbName);
if ((int)dbCheck.ExecuteScalar() == 0)
return false;
// 2) Hedef DB'ye bağlan — AbpRoles tablosunun varlığını kontrol et
csb.ConnectTimeout = 8;
using var dbConn = new SqlConnection(csb.ConnectionString);
dbConn.Open();
using var tableCheck = new SqlCommand(
"SELECT COUNT(1) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'AbpRoles'",
dbConn);
return (int)tableCheck.ExecuteScalar() > 0;
}
// Minimal Kurulum Uygulaması
public static async Task<int> RunAsync(string[] args, IConfiguration configuration)
{
Log.Warning("Veritabanı hazır değil — kurulum modu başlatılıyor.");
var builder = WebApplication.CreateBuilder(args);
var extraOrigins = (configuration["App:CorsOrigins"] ?? "")
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var baseDomain = configuration["App:BaseDomain"]?.Trim();
builder.Services.AddCors(o => o.AddPolicy("Setup", policy =>
policy.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.SetIsOriginAllowed(origin =>
{
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri)) return false;
var host = uri.Host.ToLowerInvariant();
if (host is "localhost" or "127.0.0.1" or "[::1]")
return true;
if (!string.IsNullOrWhiteSpace(baseDomain))
{
var bd = baseDomain.ToLowerInvariant();
if (host == bd || host.EndsWith("." + bd))
return true;
}
foreach (var o in extraOrigins)
if (Uri.TryCreate(o, UriKind.Absolute, out var eo) &&
eo.Host.Equals(host, StringComparison.OrdinalIgnoreCase))
return true;
return false;
})));
builder.Host.UseSerilog();
var app = builder.Build();
app.UseCors("Setup");
app.MapGet("/api/setup/status", (IConfiguration cfg) =>
Results.Ok(new { dbExists = DatabaseIsReady(cfg) }));
app.MapGet("/api/setup/migrate", async (IConfiguration cfg, IHostEnvironment env,
IHostApplicationLifetime lifetime, HttpContext ctx, CancellationToken ct) =>
{
ctx.Response.ContentType = "text/event-stream; charset=utf-8";
ctx.Response.Headers["Cache-Control"] = "no-cache, no-store";
ctx.Response.Headers["X-Accel-Buffering"] = "no";
await ctx.Response.Body.FlushAsync(ct);
async Task Send(string level, string message)
{
try
{
var payload = JsonSerializer.Serialize(new { level, message });
await ctx.Response.WriteAsync($"data: {payload}\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}
catch { }
}
if (DatabaseIsReady(cfg))
{
await Send("warn", "Veritabanı zaten hazır. Migration atlanıyor.");
await Send("done", "Tamamlandı.");
return;
}
var migratorPath = cfg["Setup:MigratorPath"]
?? Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "Sozsoft.Platform.DbMigrator"));
await Send("info", "Veritabanı migration ve seed başlatılıyor...");
await Send("info", $"Migrator yolu: {migratorPath}");
var extraArgs = cfg["Setup:MigratorArgs"] ?? "--Seed=true";
string fileName;
string arguments;
string workingDirectory;
if (migratorPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && File.Exists(migratorPath))
{
// Doğrudan DLL yolu verilmiş — "--" separator YOK, doğrudan argüman
fileName = "dotnet";
arguments = $"\"{migratorPath}\" {extraArgs}";
workingDirectory = Path.GetDirectoryName(migratorPath)!;
}
else if (Directory.Exists(migratorPath))
{
// Klasör verilmiş — içinde publish edilmiş DLL var mı?
var dllFiles = Directory.GetFiles(migratorPath, "*.DbMigrator.dll", SearchOption.TopDirectoryOnly);
if (dllFiles.Length == 0)
dllFiles = Directory.GetFiles(migratorPath, "*Migrator*.dll", SearchOption.TopDirectoryOnly);
if (dllFiles.Length > 0)
{
// Publish çıktısı — SDK gerekmez, "--" separator YOK
fileName = "dotnet";
arguments = $"\"{dllFiles[0]}\" {extraArgs}";
workingDirectory = migratorPath;
}
else
{
// Kaynak proje klasörü — geliştirme ortamı, "--" gerekli
fileName = "dotnet";
arguments = $"run --project \"{migratorPath}\" -- {extraArgs}";
workingDirectory = migratorPath;
}
}
else
{
await Send("error", $"Migrator yolu bulunamadı veya geçersiz: {migratorPath}");
await Send("done", "Hata ile sonlandı.");
return;
}
await Send("info", $"Çalıştırılıyor: {fileName} {arguments}");
Process? process = null;
try
{
process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workingDirectory,
}
};
process.Start();
async Task ReadStream(StreamReader reader, string level)
{
try
{
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync(ct);
if (line != null) await Send(level, line);
}
}
catch (OperationCanceledException) { }
}
await Task.WhenAll(
ReadStream(process.StandardOutput, "info"),
ReadStream(process.StandardError, "warn"));
await process.WaitForExitAsync(ct);
if (process.ExitCode == 0)
{
await Send("success", "Migration ve seed başarıyla tamamlandı.");
await Send("restart", "Uygulama sunucusu yeniden başlatılıyor...");
await Send("done", "Tamamlandı.");
_ = Task.Delay(1500).ContinueWith(_ => lifetime.StopApplication());
}
else
{
await Send("error", $"Migration başarısız. Çıkış kodu: {process.ExitCode}");
await Send("done", "Hata ile sonlandı.");
}
}
catch (OperationCanceledException)
{
await Send("warn", "Migration isteği iptal edildi.");
}
catch (Exception ex)
{
await Send("error", $"Migration hatası: {ex.Message}");
await Send("done", "Hata ile sonlandı.");
}
finally
{
process?.Dispose();
}
});
app.MapFallback(() => Results.StatusCode(503));
await app.RunAsync();
return 0;
}
}

View file

@ -1,4 +1,5 @@
using DevExpress.XtraReports.UI;
using Sozsoft.Platform.Enums;
namespace Sozsoft.Reports.PredefinedReports
{
@ -104,12 +105,12 @@ namespace Sozsoft.Reports.PredefinedReports
//
this.sqlDataSource1.ConnectionName = "SqlServer";
this.sqlDataSource1.Name = "sqlDataSource1";
table1.Name = "Sas_T_ReportTemplate";
table1.Name = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportTemplate));
columnExpression1.ColumnName = "HtmlContent";
columnExpression1.Table = table1;
column1.Expression = columnExpression1;
selectQuery1.Columns.Add(column1);
selectQuery1.Name = "Sas_T_ReportTemplate";
selectQuery1.Name = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportTemplate));
selectQuery1.Tables.Add(table1);
this.sqlDataSource1.Queries.AddRange(new DevExpress.DataAccess.Sql.SqlQuery[] { selectQuery1 });
this.sqlDataSource1.ResultSchemaSerializable = resources.GetString("sqlDataSource1.ResultSchemaSerializable");
@ -176,7 +177,7 @@ namespace Sozsoft.Reports.PredefinedReports
this.Detail});
this.ComponentStorage.AddRange(new System.ComponentModel.IComponent[] {
this.sqlDataSource1});
this.DataMember = "Sas_T_ReportTemplate";
this.DataMember = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportTemplate));
this.DataSource = this.sqlDataSource1;
this.Font = new DevExpress.Drawing.DXFont("Arial", 9.75F);
this.StyleSheet.AddRange(new DevExpress.XtraReports.UI.XRControlStyle[] {

View file

@ -1,4 +1,6 @@
namespace Sozsoft.Reports.PredefinedReports
using Sozsoft.Platform.Enums;
namespace Sozsoft.Reports.PredefinedReports
{
partial class TestReport
{
@ -11,8 +13,10 @@
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if(disposing && (components != null)) {
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
@ -24,7 +28,8 @@
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
DevExpress.DataAccess.Sql.SelectQuery selectQuery1 = new DevExpress.DataAccess.Sql.SelectQuery();
DevExpress.DataAccess.Sql.Column column1 = new DevExpress.DataAccess.Sql.Column();
@ -145,7 +150,7 @@
this.sqlDataSource1.ConnectionName = "SqlServer";
this.sqlDataSource1.Name = "sqlDataSource1";
columnExpression1.ColumnName = "Id";
table1.Name = "Sas_T_Sector";
table1.Name = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Sector));
columnExpression1.Table = table1;
column1.Expression = columnExpression1;
columnExpression2.ColumnName = "Name";
@ -153,7 +158,7 @@
column2.Expression = columnExpression2;
selectQuery1.Columns.Add(column1);
selectQuery1.Columns.Add(column2);
selectQuery1.Name = "Sas_T_Sector";
selectQuery1.Name = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Sector));
selectQuery1.Tables.Add(table1);
this.sqlDataSource1.Queries.AddRange(new DevExpress.DataAccess.Sql.SqlQuery[] {
selectQuery1});
@ -221,7 +226,7 @@
this.Detail});
this.ComponentStorage.AddRange(new System.ComponentModel.IComponent[] {
this.sqlDataSource1});
this.DataMember = "Sas_T_Sector";
this.DataMember = TableNameResolver.GetFullTableName(nameof(TableNameEnum.Sector));
this.DataSource = this.sqlDataSource1;
this.Font = new DevExpress.Drawing.DXFont("Arial", 9.75F);
this.StyleSheet.AddRange(new DevExpress.XtraReports.UI.XRControlStyle[] {

View file

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.DynamicServices;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -26,6 +25,24 @@ public class Program
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? ""}.json", true)
.Build();
// ── Veritabanı varlık kontrolü ────────────────────────────────────────
// Serilog SQL sink kurulmadan ve ABP modülleri yüklenmeden önce yapılmalı.
// DB yoksa minimal setup uygulaması çalıştırılır.
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Warning()
.WriteTo.Console()
.CreateLogger();
if (!SetupAppRunner.DatabaseIsReady(configuration))
{
var setupResult = await SetupAppRunner.RunAsync(args, configuration);
if (setupResult != 0)
return setupResult;
// Migration başarılı — DB artık hazır, tam ABP başlatmasına geç
Log.Warning("Migration tamamlandı — tam uygulama başlatılıyor.");
}
// ── Veritabanı mevcut — tam ABP başlatma ─────────────────────────────
var columnWriters = new Dictionary<string, ColumnWriterBase>
{

View file

@ -24,6 +24,10 @@
"StringEncryption": {
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
},
"Setup": {
"MigratorPath": "/srv/Sozsoft.Platform.DbMigrator",
"MigratorArgs": "--Seed=true"
},
"Serilog": {
"MinimumLevel": {
"Default": "Information"

View file

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
namespace Sozsoft.Platform.Controllers;
/// <summary>
/// Tam ABP uygulaması çalışırken setup durumunu döner.
/// SetupAppRunner'ın aynı endpoint'i DB hazır olmadığında dbExists=false döner.
/// </summary>
[ApiController]
[Route("api/setup")]
public class SetupController : AbpControllerBase
{
[HttpGet("status")]
public IActionResult Status() => Ok(new { dbExists = true });
}

71
claude.md Normal file
View file

@ -0,0 +1,71 @@
# Sozsoft Platform - Claude Instructions
This file provides Claude-specific operating rules for this repository.
Primary source of truth for platform behavior is:
- `.github/instructions/ai.instructions.md`
If there is any conflict, follow `.github/instructions/ai.instructions.md`.
## Purpose
- Maximize delivery through runtime configuration.
- Minimize custom code.
- Preserve platform consistency, security, and tenant isolation.
Primary principle: Configuration first, code last.
## Mandatory Decision Order
For every request, evaluate and propose in this order:
1. Dynamic configuration with existing ListForm ecosystem
2. SQL Query Manager + Custom Endpoint
3. Dynamic Service
4. Code change (last resort, justification required)
## Non-Negotiable Rules
1. Do not propose new custom React component/page development for standard feature requests.
2. Build new screens using platform configuration mechanisms.
3. Every proposal must include tenant and permission design.
4. Never bypass platform authorization patterns.
5. Never hardcode secrets, tenant IDs, or connection strings.
Exception:
- Custom React/backend code is allowed only when the user explicitly requests implementation and configuration is insufficient.
- In such cases, explain why configuration-first options are not enough.
## Architecture Guardrails
- Backend: Respect ABP module boundaries and explicit, auditable permissions.
- Frontend: Use existing dynamic view infrastructure and metadata-driven behavior.
- Data: Use parameterized SQL patterns and enforce tenant-safe access.
## Dynamic Platform Expectations
- Menus and routes are database-driven.
- Dynamic List/Form/Component infrastructure is the default solution path.
- Keep route, menu, permission, and datasource mappings coherent.
## Security and Compliance
- Enforce RBAC and permission-driven visibility in all layers.
- Never output real credentials, tokens, keys, or secrets.
- Use placeholders in examples.
- Maintain tenant isolation in every query and action.
## Response Contract
When producing an implementation proposal, include:
1. Goal
2. Decision flow result (which step used)
3. Artifacts to configure
4. SQL/query/endpoint design (if needed)
5. Menu + route + component mapping
6. Permission and role mapping
7. Tenant isolation notes
8. Validation and test checklist
9. Rollback strategy

View file

@ -15,7 +15,7 @@
}
.drawer-body {
@apply p-6 h-full overflow-y-auto;
@apply p-4 h-full overflow-y-auto;
}
.drawer-footer {

View file

@ -1,5 +1,6 @@
import React from 'react';
import { ComponentInfo } from '../../proxy/developerKit/componentInfo';
import { Button } from '../ui';
interface ComponentSelectorProps {
components: ComponentInfo[];
@ -20,13 +21,15 @@ const ComponentSelector: React.FC<ComponentSelectorProps> = ({
<label className="block text-sm font-medium text-gray-700">
Select Component
</label>
<button
<Button
variant='solid'
size="xs"
onClick={onRefresh}
className="px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 transition-colors"
title="Refresh component list"
>
Refresh
</button>
</Button>
</div>
<select
value={selectedComponentId || ''}

View file

@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import TailwindModal from "./TailwindModal";
import { ComponentInfo, HookInfo, PropertyInfo } from "../../proxy/developerKit/componentInfo";
import { getComponentDefinition } from "./data/componentDefinitions";
import { Button } from "../ui";
interface PropertyPanelProps {
selectedComponent: ComponentInfo | null;
@ -525,7 +526,9 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
</div>
</div>
{/* Sil Butonu */}
<button
<Button
variant="solid"
size="xs"
className="mr-2 px-3 py-1 rounded bg-red-500 text-white hover:bg-red-600 transition-colors text-sm"
onClick={() => {
if (selectedComponent) {
@ -541,28 +544,32 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
title="Komponenti Sil"
>
Sil
</button>
</Button>
</div>
{/* Footer Action Buttons - her iki tabda da sabit */}
<div className="p-4 border-t">
<div className="flex gap-2">
<button
<Button
size="xs"
variant="solid"
onClick={
activeTab === "props"
? handleApplyPropChanges
: handleApplyHookChanges
}
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
className={`flex-1 px-4 py-2 rounded-md font-medium transition-colors ${
className={`flex-1 rounded-md font-medium transition-colors ${
(activeTab === "props" ? hasChanges : hasHookChanges)
? "bg-green-500 text-white hover:bg-green-600"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
>
Uygula
</button>
<button
</Button>
<Button
size="xs"
variant="default"
onClick={
activeTab === "props"
? handleResetChanges
@ -572,20 +579,20 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
}
}
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
className={`flex-1 px-4 py-2 rounded-md font-medium transition-colors ${
className={`flex-1 rounded-md font-medium transition-colors ${
(activeTab === "props" ? hasChanges : hasHookChanges)
? "bg-red-500 text-white hover:bg-red-600"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
>
İptal
</button>
</Button>
</div>
</div>
{/* Content */}
{activeTab === "props" && (
<div className="flex-1 overflow-y-auto p-4 max-h-[calc(100vh-200px)]">
<div className="flex-1 text-black overflow-y-auto p-4 max-h-[calc(100vh-200px)]">
<h3 className="text-md font-medium text-gray-800 mb-4">Properties</h3>
{/* Properties */}
{properties.length > 0 && (

View file

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { searchTailwindClasses, TAILWIND_CLASSES } from './data/tailwindClasses';
import { Button } from '../ui';
interface TailwindModalProps {
isOpen: boolean;
@ -61,7 +62,7 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
</div>
{/* Search and Filter */}
<div className="p-4 border-b bg-gray-50">
<div className="p-4 border-b bg-gray-50 text-black">
<div className="flex gap-4 mb-4">
<input
type="text"
@ -106,25 +107,15 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
key={`${className}-${index}`}
className="group relative"
>
<button
<Button
variant='default'
onClick={() => handleClassSelect(className)}
className="w-full text-left px-3 py-2 text-sm bg-gray-100 hover:bg-blue-100 rounded border hover:border-blue-300 transition-colors"
>
<span className="font-mono text-xs text-gray-600">
{className}
</span>
</button>
{/* Add to current value button */}
{currentValue && (
<button
onClick={() => handleAddToCurrentValue(className)}
className="absolute top-1 right-1 w-6 h-6 bg-blue-500 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity hover:bg-blue-600"
title="Add to current value"
>
+
</button>
)}
</Button>
</div>
))}
</div>
@ -137,18 +128,20 @@ const TailwindModal: React.FC<TailwindModalProps> = ({
{filteredClasses.length} classes found
</div>
<div className="flex gap-2">
<button
<Button
variant='default'
onClick={() => onSelectClass('')}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-colors"
>
Clear
</button>
<button
</Button>
<Button
variant='solid'
onClick={onClose}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
Close
</button>
</Button>
</div>
</div>
</div>

View file

@ -283,7 +283,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<thead className="bg-slate-100 sticky top-0">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
{translate('::App.Listforms.ImportManager.Column')}
{translate('::App.Listform.ListformField.Column')}
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
{translate('::ListForms.ListFormEdit.Type')}

View file

@ -60,6 +60,11 @@ const Layout = () => {
}, [routes, currentPath])
const AppLayout = useMemo(() => {
// 0) Setup path ise minimal blank layout
if (currentPath === '/setup') {
return layouts[LAYOUT_TYPE_BLANK]
}
// 1) Admin path ise, route bulunmasa bile admin layout'u göster
if (isAdminPath) {
return layouts[layoutType]
@ -78,7 +83,7 @@ const Layout = () => {
return AuthLayout
}
return PublicLayout
}, [isAdminPath, route, layoutType, authenticated])
}, [isAdminPath, route, layoutType, authenticated, currentPath])
if (loading) {
return (

View file

@ -155,7 +155,7 @@ export const Cart: React.FC<CartProps> = ({
<div className="border-t border-gray-200 p-6">
<div className="flex justify-between items-center mb-4">
<span className="text-lg font-semibold text-gray-900">
{translate('::Public.payment.summary.total')}
{translate('::App.Listform.ListformField.Total')}
</span>
<span className="text-xl font-bold text-blue-600">
{formatPrice(cartState.total)}

View file

@ -436,7 +436,7 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
</div>
)}
<div className="flex justify-between text-base font-bold pt-2 text-gray-900">
<span>{translate('::Public.payment.summary.total')}</span>
<span>{translate('::App.Listform.ListformField.Total')}</span>
<span className="text-blue-600">{formatPrice(finalTotal)}</span>
</div>
</div>
@ -449,7 +449,7 @@ export const PaymentForm: React.FC<PaymentFormProps> = ({
className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg"
>
<FaArrowLeft className="w-4 h-4 mr-2" />
{translate('::Public.payment.buttons.back')}
{translate('::Back')}
</button>
<button
type="submit"

View file

@ -154,7 +154,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
</div>
{globalPeriod > 1 && (
<div className="text-lg font-semibold text-blue-600 mt-1">
{translate('::Public.payment.summary.total')} {formatPrice(getTotalPrice())}
{translate('::App.Listform.ListformField.Total')} {formatPrice(getTotalPrice())}
<span className="text-sm font-normal text-gray-500 ml-1">{getPeriodText()}</span>
</div>
)}

View file

@ -479,7 +479,7 @@ export const TenantForm: React.FC<TenantFormProps> = ({ onSubmit }) => {
className="flex items-center px-6 py-3 border border-gray-300 text-gray-700 rounded-lg"
>
<FaArrowLeft className="w-4 h-4 mr-2" />
{translate('::Public.payment.buttons.back')}
{translate('::Back')}
</button>
<button
type="submit"

View file

@ -1,8 +1,9 @@
import Tooltip from '@/components/ui/Tooltip'
import { AI_ASSISTANT } from '@/constants/permission.constant'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
import { FcAssistant, FcHeadset } from 'react-icons/fc'
import { FcHeadset } from 'react-icons/fc'
import { useNavigate } from 'react-router-dom'
const AiAssistant = () => {
@ -10,14 +11,14 @@ const AiAssistant = () => {
const navigate = useNavigate()
const { checkPermissions } = usePermission()
const canViewAi = checkPermissions(['App.AiBot.Asistant'])
const canViewAi = checkPermissions([AI_ASSISTANT])
if (!canViewAi) {
return null
}
return (
<Tooltip title={translate('::App.AiBot.Asistant')}>
<Tooltip title={translate('::' + AI_ASSISTANT)}>
<div
onClick={() => navigate(ROUTES_ENUM.protected.admin.ai)}
className="flex items-center justify-center text-2xl m-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer transition-colors duration-200"

View file

@ -1,6 +1,7 @@
import classNames from 'classnames'
import Drawer from '@/components/ui/Drawer'
import SidePanelContent, { SidePanelContentProps } from './SidePanelContent'
import CopyButton from '../ThemeConfigurator/CopyButton'
import withHeaderItem from '@/utils/hoc/withHeaderItem'
import { useStoreState, useStoreActions } from '@/store'
import type { CommonProps } from '@/proxy/common'
@ -41,7 +42,12 @@ const _SidePanel = (props: SidePanelProps) => {
</div>
</Tooltip>
<Drawer
title={translate('::SidePanel.Title')}
title={
<div className="flex items-center justify-between gap-x-2">
<h4>{translate('::SidePanel.Title')}</h4>
<CopyButton />
</div>
}
isOpen={panelExpand}
placement={direction === 'rtl' ? 'left' : 'right'}
width={375}

View file

@ -8,18 +8,43 @@ import { useSetting } from '@/utils/hooks/useSetting'
import useTabFocus from '@/utils/hooks/useTabFocus'
import { useEffect } from 'react'
import { Helmet } from 'react-helmet'
import { useNavigate, useLocation } from 'react-router-dom'
import { getSetupStatus } from '@/services/setup.service'
import { ROUTES_ENUM } from '@/routes/route.constant'
let didInit = false
const Theme = (props: CommonProps) => {
// ABP App Config'i uygulama acilirken al
const { getConfig } = useStoreActions((a) => a.abpConfig)
const { setSetupMode } = useStoreActions((a) => a.base.common)
const navigate = useNavigate()
const location = useLocation()
useEffect(() => {
if (!didInit) {
didInit = true
// Direkt /setup'a gelindiyse — hemen setupMode=true yap (loading gate açılsın)
if (location.pathname === ROUTES_ENUM.setup) {
setSetupMode(true)
return
}
// Veritabanı var mı kontrol et; yoksa setup sayfasına yönlendir
getSetupStatus()
.then((res) => {
if (!res.data.dbExists) {
setSetupMode(true)
navigate(ROUTES_ENUM.setup, { replace: true })
} else {
getConfig(false)
}
})
.catch(() => {
getConfig(false)
})
}
}, [])
function getThemeStyle() {

View file

@ -3,9 +3,12 @@ import Button from '@/components/ui/Button'
import toast from '@/components/ui/toast'
import { themeConfig } from '@/proxy/theme/theme.config'
import { useStoreState } from '@/store'
import { FaSave } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
const CopyButton = () => {
const theme = useStoreState((state) => state.theme)
const { translate } = useLocalization()
const handleCopy = () => {
const config = {
@ -31,9 +34,14 @@ const CopyButton = () => {
}
return (
<Button block variant="solid" onClick={handleCopy}>
Copy config
</Button>
<Button
shape="circle"
variant="plain"
size="xs"
onClick={handleCopy}
title={translate('::SidePanel.SaveConfig')}
icon={<FaSave />}
/>
)
}

View file

@ -12,6 +12,7 @@ import {
LAYOUT_TYPE_SIMPLE,
LAYOUT_TYPE_DECKED,
LAYOUT_TYPE_BLANK,
NAV_MODE_TRANSPARENT,
} from '@/constants/theme.constant'
import type { LayoutType } from '@/proxy/theme/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -57,7 +58,7 @@ const layouts = [
const LayoutSwitcher = () => {
const type = useStoreState((state) => state.theme.layout.type)
const { setLayout } = useStoreActions((actions) => actions.theme)
const { setLayout, setNavMode } = useStoreActions((actions) => actions.theme)
const onLayoutSelect = (val: LayoutType) => {
setLayout(val)

View file

@ -1,23 +1,31 @@
import Radio from '@/components/ui/Radio'
import { useStoreState, useStoreActions } from '@/store'
import { NAV_MODE_THEMED } from '@/constants/theme.constant'
type NavModeParam = 'default' | 'themed'
import {
NAV_MODE_DARK,
NAV_MODE_LIGHT,
NAV_MODE_THEMED,
NAV_MODE_TRANSPARENT,
} from '@/constants/theme.constant'
import { NavMode } from '@/proxy/theme/models'
import { availableNavColorLayouts } from '@/proxy/theme/theme.config'
const NavModeSwitcher = () => {
const navMode = useStoreState((state) => state.theme.navMode)
const { navMode, layout } = useStoreState((state) => state.theme)
const { setNavMode } = useStoreActions((actions) => actions.theme)
const onSetNavMode = (val: NavModeParam) => {
const onSetNavMode = (val: NavMode) => {
setNavMode(val)
}
return (
<Radio.Group
value={navMode === NAV_MODE_THEMED ? NAV_MODE_THEMED : 'default'}
onChange={onSetNavMode}
>
<Radio value="default">Default</Radio>
<Radio.Group value={navMode} onChange={onSetNavMode}>
<Radio value={NAV_MODE_TRANSPARENT}>Transparent</Radio>
{!availableNavColorLayouts.includes(layout.type) && (
<>
<Radio value={NAV_MODE_LIGHT}>Light</Radio>
<Radio value={NAV_MODE_DARK}>Dark</Radio>
</>
)}
<Radio value={NAV_MODE_THEMED}>Themed</Radio>
</Radio.Group>
)

View file

@ -0,0 +1,124 @@
import { HiCheck } from 'react-icons/hi'
import { components } from 'react-select'
import { Select, toast } from '@/components/ui'
import { styleMapOptions } from '@/views/admin/listForm/edit/options'
import Notification from '@/components/ui/Notification'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreActions, useStoreState } from '@/store'
import { updateSettingValues } from '@/services/setting-ui.service'
import React from 'react'
const StyleSwitcher = ({ onStyleChange }: { onStyleChange?: () => void }) => {
const { translate } = useLocalization()
const { setMode, setStyle, setThemeColor, setThemeColorLevel, abpConfig } = useStoreActions(
(actions) => ({
setMode: actions.theme.setMode,
setStyle: actions.theme.setStyle,
setThemeColor: actions.theme.setThemeColor,
setThemeColorLevel: actions.theme.setThemeColorLevel,
abpConfig: actions.abpConfig.getConfig,
}),
)
const { style, themeColor, primaryColorLevel } = useStoreState((state) => state.theme)
const onSetStyle = React.useCallback(
async (val: any) => {
setStyle(val.value)
setMode(val.value.includes('.dark') ? 'dark' : 'light')
setThemeColor(val.color?.color)
setThemeColorLevel(val.color?.colorLevel)
//Update setting value
const values: Record<string, string> = {
App_SiteManagement_Theme_Style: val.value,
}
const resp = await updateSettingValues(values)
if (resp.status !== 204) {
toast.push(<Notification title={resp?.error?.message} type="danger" />, {
placement: 'top-end',
})
}
abpConfig(false)
if (onStyleChange) onStyleChange()
},
[setStyle, setMode, abpConfig, translate, onStyleChange],
)
// Custom Option
const CustomSelectOption = ({ innerProps, label, data, isSelected }: any) => {
const { border, fill } = data.color
return (
<div
className={`flex items-center justify-between p-2 cursor-pointer ${
isSelected ? 'bg-gray-100 dark:bg-gray-500' : 'hover:bg-gray-50 dark:hover:bg-gray-600'
}`}
{...innerProps}
>
<div className="flex items-center">
<span
style={{
display: 'inline-block',
width: 20,
height: 20,
borderRadius: '50%',
background: fill,
border: `4px solid ${border}`,
boxSizing: 'border-box',
marginRight: 8,
boxShadow: '0 0 0 1px #ccc', // dış 1px border
}}
/>
<span>{label}</span>
</div>
{isSelected && <HiCheck className="text-emerald-500 text-xl" />}
</div>
)
}
// Custom Control
const CustomControl = ({ children, ...props }: any) => {
const selected = props.getValue()[0]
const { border, fill } = selected?.color
return (
<components.Control {...props}>
{selected ? (
<div
className="flex items-center"
style={{ marginLeft: 8, border: '1px solid #ccc', borderRadius: '50%' }}
>
<span
style={{
display: 'inline-block',
width: 20,
height: 20,
borderRadius: '50%',
background: fill,
border: `4px solid ${border}`,
}}
/>
</div>
) : null}
{children}
</components.Control>
)
}
return (
<Select
value={styleMapOptions.find((o) => o.value === style)}
options={styleMapOptions}
onChange={(option: any) => {
onSetStyle(option)
}}
components={{
Option: CustomSelectOption,
Control: CustomControl,
}}
/>
)
}
export default StyleSwitcher

View file

@ -3,32 +3,30 @@ import LayoutSwitcher from './LayoutSwitcher'
import ThemeSwitcher from './ThemeSwitcher'
import DirectionSwitcher from './DirectionSwitcher'
import NavModeSwitcher from './NavModeSwitcher'
import CopyButton from './CopyButton'
import StyleSwitcher from './StyleSwitcher'
import React, { useState, useCallback } from 'react'
import { useLocalization } from '@/utils/hooks/useLocalization'
export type ThemeConfiguratorProps = {
callBackClose?: () => void
}
const ThemeConfigurator = ({ callBackClose }: ThemeConfiguratorProps) => {
const { translate } = useLocalization()
const [modeKey, setModeKey] = useState(0)
// StyleSwitcher'dan tetiklenecek
const handleStyleChange = useCallback(() => {
setModeKey((prev) => prev + 1)
}, [])
return (
<div className="flex flex-col h-full justify-between">
<div className="flex flex-col gap-y-10 mb-6">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-y-3 mb-2">
<div>
<h6>{translate('::SidePanel.Mode')}</h6>
<span>{translate('::SidePanel.Mode.Description')}</span>
</div>
<ModeSwitcher />
</div>
<div className="flex items-center justify-between">
<div>
<h6>{translate('::SidePanel.Direction')}</h6>
<span>{translate('::SidePanel.Direction.Description')}</span>
</div>
<DirectionSwitcher callBackClose={callBackClose} />
<h6 className="mb-3">{translate('::App.SiteManagement.Theme.Style')}</h6>
<StyleSwitcher onStyleChange={handleStyleChange} />
</div>
<div>
<h6 className="mb-3">{translate('::SidePanel.NavMode')}</h6>
@ -38,12 +36,25 @@ const ThemeConfigurator = ({ callBackClose }: ThemeConfiguratorProps) => {
<h6 className="mb-3">{translate('::SidePanel.Themed')}</h6>
<ThemeSwitcher />
</div>
<div className="flex items-center justify-between">
<div>
<h6>{translate('::SidePanel.Mode')}</h6>
<span>{translate('::SidePanel.Mode.Description')}</span>
</div>
<ModeSwitcher key={modeKey} />
</div>
<div className="flex items-center justify-between">
<div>
<h6>{translate('::SidePanel.Direction')}</h6>
<span>{translate('::SidePanel.Direction.Description')}</span>
</div>
<DirectionSwitcher callBackClose={callBackClose} />
</div>
<div>
<h6 className="mb-3">{translate('::SidePanel.Layout')}</h6>
<LayoutSwitcher />
</div>
</div>
<CopyButton />
</div>
)
}

View file

@ -28,6 +28,7 @@ const colorList: ColorList[] = [
{ label: 'Green', value: 'green' },
{ label: 'Emerald', value: 'emerald' },
{ label: 'Teal', value: 'teal' },
{ label: 'Gray', value: 'gray' },
{ label: 'Cyan', value: 'cyan' },
{ label: 'Sky', value: 'sky' },
{ label: 'Blue', value: 'blue' },
@ -59,13 +60,13 @@ const ColorBadge = ({ className, themeColor }: { className?: string; themeColor:
const CustomSelectOption = ({ innerProps, label, data, isSelected }: OptionProps<ColorList>) => {
return (
<div
className={`flex items-center justify-between p-2 ${
className={`flex items-center justify-between p-2 cursor-pointer ${
isSelected ? 'bg-gray-100 dark:bg-gray-500' : 'hover:bg-gray-50 dark:hover:bg-gray-600'
}`}
{...innerProps}
>
<div className="flex items-center gap-2">
<ColorBadge themeColor={data.value} />
<ColorBadge themeColor={data.value} className='p-3' />
<span>{label}</span>
</div>
{isSelected && <FaCheck className="text-emerald-500 text-xl" />}
@ -80,7 +81,7 @@ const CustomControl = ({ children, ...props }: ControlProps<ColorList>) => {
return (
<Control {...props}>
{selected && <ColorBadge themeColor={themeColor} className="ltr:ml-4 rtl:mr-4" />}
{selected && <ColorBadge themeColor={themeColor} className="ml-2 p-3" />}
{children}
</Control>
)

View file

@ -1 +1,2 @@
export const GLOBAL_SEARCH = 'App.Settings.GlobalSearch'
export const GLOBAL_SEARCH = 'App.Definitions.GlobalSearch'
export const AI_ASSISTANT = 'App.Definitions.AiBot.Asistant'

View file

@ -1,3 +1,5 @@
export const PREFIX = "App";
export const DIR_RTL = 'rtl'
export const DIR_LTR = 'ltr'
export const MODE_LIGHT = 'light'

View file

@ -98,3 +98,17 @@ div.dialog-after-open > div.dialog-content.maximized {
position: absolute !important;
left: -9999px !important;
}
.dx-toolbar .dx-toolbar-after .dx-menu,
.dx-toolbar .dx-toolbar-after .dx-menu .dx-menu-item,
.dx-toolbar .dx-toolbar-after .dx-menu .dx-menu-item-content,
.dx-toolbar .dx-toolbar-after .dx-menu .dx-menu-item-wrapper {
display: flex !important;
align-items: center !important;
padding: 0 0 !important;
font-weight: bold !important;
}
.dx-datagrid-header-panel {
padding: 0 0 !important;
}

View file

@ -4,3 +4,23 @@ export interface AiDto extends FullAuditedEntityDto<string> {
name: string
apiUrl?: string
}
export type ChatType = 'chat' | 'query' | 'analyze'
export interface BaseContent {
type: ChatType
question: string
sql: string | null
answer: string | any[]
chart?: string
error?: string
}
export type MessageContent = string | BaseContent
export interface Message {
role: 'user' | 'assistant'
content: MessageContent
/** ISO string */
createdAt?: string
}

View file

@ -26,6 +26,8 @@ export interface AuditLogDto {
applicationName: string
userId?: string
userName?: string
tenantId?: string
tenantName?: string
executionTime: string
executionDuration: number
clientIpAddress?: string

View file

@ -196,6 +196,7 @@ export interface ColumnFilterDto {
export interface ColumnFormatDto extends AuditedEntityDto<string> {
fieldName?: string
captionName?: string
placeHolder?: string
readOnly: boolean
visible: boolean
isActive: boolean
@ -444,6 +445,7 @@ export interface GridEditingDto {
allowDeleting: boolean
allowAllDeleting: boolean
allowAdding: boolean
allowDuplicate: boolean
useIcons: boolean
confirmDelete: boolean
newRowPosition?: NewRowPosition

View file

@ -33,4 +33,5 @@ export type ThemeConfig = {
type: LayoutType
sideNavCollapse: boolean
}
style: string
}

View file

@ -1,4 +1,4 @@
import { THEME_ENUM } from '@/constants/theme.constant'
import { LAYOUT_TYPE_BLANK, LAYOUT_TYPE_DECKED, LAYOUT_TYPE_SIMPLE, THEME_ENUM } from '@/constants/theme.constant'
import { ThemeConfig } from '@/proxy/theme/models'
export const themeConfig: ThemeConfig = {
@ -14,4 +14,7 @@ export const themeConfig: ThemeConfig = {
type: THEME_ENUM.LAYOUT_TYPE_SIMPLE,
sideNavCollapse: false,
},
style: 'dx.material.blue.light.compact',
}
export const availableNavColorLayouts = [LAYOUT_TYPE_DECKED, LAYOUT_TYPE_SIMPLE, LAYOUT_TYPE_BLANK]

View file

@ -1,6 +1,6 @@
// DynamicRouter.tsx
import React from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
import { mapDynamicRoutes } from './dynamicRouteLoader'
import { useDynamicRoutes } from './dynamicRoutesContext'
import { useComponents } from '@/contexts/ComponentContext'
@ -13,15 +13,18 @@ import { hasSubdomain } from '@/utils/subdomain'
// AccessDenied ve NotFound'u dinamiklikten çıkarıyoruz
const AccessDenied = React.lazy(() => import('@/views/AccessDenied'))
const NotFound = React.lazy(() => import('@/views/NotFound'))
const DatabaseSetup = React.lazy(() => import('@/views/setup/DatabaseSetup'))
export const DynamicRouter: React.FC = () => {
const { routes, loading, error } = useDynamicRoutes()
const { registeredComponents, renderComponent, isComponentRegistered } = useComponents()
const location = useLocation()
const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes])
if (loading) return <div>Loading...</div>
if (error) return <div>Hata: {error}</div>
// /setup path'inde loading bekleme — setup route her zaman erişilebilir olmalı
if (loading && location.pathname !== '/setup') return <div>Loading...</div>
if (error && location.pathname !== '/setup') return <div>Hata: {error}</div>
return (
<Routes>
@ -126,6 +129,16 @@ export const DynamicRouter: React.FC = () => {
</React.Suspense>
}
/>
{/* İlk kurulum — veritabanı mevcut değilse gösterilir */}
<Route
path={ROUTES_ENUM.setup}
element={
<React.Suspense fallback={<div>Loading...</div>}>
<DatabaseSetup />
</React.Suspense>
}
/>
</Routes>
)
}

View file

@ -19,6 +19,7 @@ export const useDynamicRoutes = () => {
export const DynamicRoutesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const extraProperties = useStoreState((state) => state.abpConfig?.config?.extraProperties)
const setupMode = useStoreState((state) => state.base.common.setupMode)
const [routes, setRoutes] = useState<RouteDto[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@ -48,8 +49,12 @@ export const DynamicRoutesProvider: React.FC<{ children: React.ReactNode }> = ({
useEffect(() => {
if (extraProperties) {
loadRoutesFromConfig()
} else if (setupMode) {
// Veritabanı mevcut değil — setup modunda loading'i kapat
setLoading(false)
setRoutes([])
}
}, [extraProperties])
}, [extraProperties, setupMode])
return (
<DynamicRoutesContext.Provider value={{ routes, loading, error, reload: loadRoutesFromConfig }}>

View file

@ -1,4 +1,5 @@
export const ROUTES_ENUM = {
setup: '/setup',
public: {
home: '/home',
about: '/about',

View file

@ -5,10 +5,13 @@ import {
} from '../proxy/config/models'
import apiService from './api.service'
export const applicationConfigurationUrl = (includeLocalizationResources: boolean) =>
`/api/abp/application-configuration?includeLocalizationResources=${includeLocalizationResources}`
export const getAppConfig = (includeLocalizationResources: boolean) =>
apiService.fetchData<ApplicationConfigurationDto>({
method: 'GET',
url: `/api/abp/application-configuration?includeLocalizationResources=${includeLocalizationResources}`,
url: applicationConfigurationUrl(includeLocalizationResources),
})
export const getLocalizations = ({

View file

@ -1,573 +0,0 @@
import { ClassroomAttendanceDto, ClassroomChatDto, HandRaiseDto } from '@/proxy/classroom/models'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { store } from '@/store/store'
import * as signalR from '@microsoft/signalr'
import { toast } from '@/components/ui'
import Notification from '@/components/ui/Notification'
export class SignalRService {
private connection!: signalR.HubConnection
private isConnected: boolean = false
private currentSessionId?: string
private isKicked: boolean = false
private onAttendanceUpdate?: (record: ClassroomAttendanceDto) => void
private onParticipantJoined?: (
userId: string,
name: string,
isTeacher: boolean,
isActive: boolean,
) => void
private onParticipantLeft?: (payload: {
userId: string
sessionId: string
userName: string
}) => void
private onChatMessage?: (message: ClassroomChatDto) => void
private onParticipantMuted?: (userId: string, isMuted: boolean) => void
private onHandRaiseReceived?: (studentId: string) => void
private onHandRaiseDismissed?: (studentId: string) => void
private onOfferReceived?: (fromUserId: string, offer: RTCSessionDescriptionInit) => void
private onAnswerReceived?: (fromUserId: string, answer: RTCSessionDescriptionInit) => void
private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void
private onForceCleanup?: () => void
constructor() {
const { auth } = store.getState()
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, {
accessTokenFactory: () => auth.session.token || '',
})
.configureLogging(signalR.LogLevel.Information)
.build()
this.setupEventHandlers()
}
private setupEventHandlers() {
if (!this.connection) return
this.connection.on('AttendanceUpdated', (record: ClassroomAttendanceDto) => {
this.onAttendanceUpdate?.(record)
})
this.connection.on(
'ParticipantJoined',
(userId: string, name: string, isTeacher: boolean, isActive: boolean) => {
this.onParticipantJoined?.(userId, name, isTeacher, isActive)
},
)
this.connection.on(
'ParticipantLeft',
(payload: { userId: string; sessionId: string; userName: string }) => {
this.onParticipantLeft?.(payload)
},
)
this.connection.on('ChatMessage', (message: any) => {
this.onChatMessage?.(message)
})
this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => {
this.onParticipantMuted?.(userId, isMuted)
})
this.connection.on('HandRaiseReceived', (payload: any) => {
this.onHandRaiseReceived?.(payload.studentId)
})
this.connection.on('HandRaiseDismissed', (payload: any) => {
this.onHandRaiseDismissed?.(payload.studentId)
})
this.connection.on('ReceiveOffer', (fromUserId: string, offer: RTCSessionDescriptionInit) => {
this.onOfferReceived?.(fromUserId, offer)
})
this.connection.on('ReceiveAnswer', (fromUserId: string, answer: RTCSessionDescriptionInit) => {
this.onAnswerReceived?.(fromUserId, answer)
})
this.connection.on(
'ReceiveIceCandidate',
(fromUserId: string, candidate: RTCIceCandidateInit) => {
this.onIceCandidateReceived?.(fromUserId, candidate)
},
)
this.connection.onreconnected(async () => {
this.isConnected = true
toast.push(<Notification title="🔄 Bağlantı tekrar kuruldu" type="success" />, {
placement: 'top-end',
})
if (this.currentSessionId && store.getState().auth.user) {
const u = store.getState().auth.user
await this.joinClass(this.currentSessionId, u.id, u.name, u.role === 'teacher', true)
}
})
this.connection.onclose(async () => {
if (this.isKicked) {
toast.push(
<Notification title="⚠️ Bağlantı koptu, yeniden bağlanılıyor..." type="warning" />,
{ placement: 'top-end' },
)
this.isConnected = false
this.currentSessionId = undefined
return
}
this.isConnected = false
try {
if (this.currentSessionId) {
await this.connection.invoke('LeaveClass', this.currentSessionId)
}
} finally {
this.currentSessionId = undefined
}
})
this.connection.on('Error', (message: string) => {
toast.push(<Notification title={`❌ Hata: ${message}`} type="danger" />, {
placement: 'top-end',
})
})
this.connection.on('Warning', (message: string) => {
toast.push(<Notification title={`⚠️ Uyarı: ${message}`} type="warning" />, {
placement: 'top-end',
})
})
this.connection.on('Info', (message: string) => {
toast.push(<Notification title={` Bilgi: ${message}`} type="info" />, {
placement: 'top-end',
})
})
this.connection.onreconnecting(() => {
if (this.isKicked) {
toast.push(
<Notification
title="❌ Sınıftan çıkarıldığınız için yeniden bağlanma engellendi"
type="danger"
/>,
)
this.connection.stop()
throw new Error('Reconnect blocked after kick')
}
})
this.connection.on('ForceDisconnect', async (message: string) => {
this.isKicked = true
toast.push(<Notification title={`❌ Sınıftan çıkarıldınız: ${message}`} type="danger" />, {
placement: 'top-end',
})
if (this.onForceCleanup) {
this.onForceCleanup()
}
try {
await this.connection.stop()
} catch {}
this.isConnected = false
if (this.currentSessionId && store.getState().auth.user) {
this.onParticipantLeft?.({
userId: store.getState().auth.user.id,
sessionId: this.currentSessionId,
userName: store.getState().auth.user.name,
})
}
this.currentSessionId = undefined
window.location.href = ROUTES_ENUM.protected.coordinator.classroom.classes
})
}
async start(): Promise<void> {
try {
const startPromise = this.connection.start()
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000),
)
await Promise.race([startPromise, timeout])
this.isConnected = true
toast.push(<Notification title="✅ Bağlantı kuruldu" type="success" />, {
placement: 'top-end',
})
} catch {
toast.push(
<Notification
title="⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin."
type="danger"
/>,
{ placement: 'top-end' },
)
this.isConnected = false
}
}
async joinClass(
sessionId: string,
userId: string,
userName: string,
isTeacher: boolean,
isActive: boolean,
): Promise<void> {
if (!this.isConnected) {
toast.push(
<Notification
title="⚠️ Bağlantı yok. Sınıfa katılmadan önce bağlantıyı kontrol edin."
type="warning"
/>,
)
return
}
this.currentSessionId = sessionId
try {
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
} catch {
toast.push(<Notification title="❌ Sınıfa katılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
async leaveClass(sessionId: string): Promise<void> {
const { auth } = store.getState()
if (!this.isConnected) {
this.onParticipantLeft?.({ userId: auth.user.id, sessionId, userName: auth.user.name })
return
}
try {
await this.connection.invoke('LeaveClass', sessionId)
this.currentSessionId = undefined
} catch {
toast.push(<Notification title="⚠️ Çıkış başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async sendChatMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: ClassroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
messageType: 'public',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendChatMessage',
sessionId,
senderId,
senderName,
message,
isTeacher,
'public',
)
} catch {
toast.push(<Notification title="❌ Mesaj gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async sendPrivateMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
recipientId: string,
recipientName: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: ClassroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
recipientId,
recipientName,
messageType: 'private',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendPrivateMessage',
sessionId,
senderId,
senderName,
message,
recipientId,
recipientName,
isTeacher,
'private',
)
} catch {
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async sendAnnouncement(
sessionId: string,
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: ClassroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
messageType: 'announcement',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendAnnouncement',
sessionId,
senderId,
senderName,
message,
isTeacher,
)
} catch {
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async muteParticipant(
sessionId: string,
userId: string,
isMuted: boolean,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onParticipantMuted?.(userId, isMuted)
}, 100)
return
}
try {
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
} catch {
toast.push(<Notification title="⚠️ Katılımcı susturulamadı" type="warning" />, {
placement: 'top-end',
})
}
}
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseReceived?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
} catch {
toast.push(<Notification title="❌ El kaldırma başarısız" type="danger" />, {
placement: 'top-end',
})
}
}
async kickParticipant(sessionId: string, participantId: string, userName: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onParticipantLeft?.({ userId: participantId, sessionId, userName })
}, 100)
return
}
try {
await this.connection.invoke('KickParticipant', sessionId, participantId)
} catch {
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
async approveHandRaise(sessionId: string, studentId: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseDismissed?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
} catch {
toast.push(<Notification title="⚠️ El kaldırma onayı başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async dismissHandRaise(sessionId: string, studentId: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseDismissed?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
} catch {
toast.push(<Notification title="⚠️ El indirme başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async sendOffer(sessionId: string, targetUserId: string, offer: RTCSessionDescriptionInit) {
if (!this.isConnected) return
await this.connection.invoke('SendOffer', sessionId, targetUserId, offer)
}
async sendAnswer(sessionId: string, targetUserId: string, answer: RTCSessionDescriptionInit) {
if (!this.isConnected) return
await this.connection.invoke('SendAnswer', sessionId, targetUserId, answer)
}
async sendIceCandidate(sessionId: string, targetUserId: string, candidate: RTCIceCandidateInit) {
if (!this.isConnected) return
await this.connection.invoke('SendIceCandidate', sessionId, targetUserId, candidate)
}
setExistingParticipantsHandler(callback: (participants: any[]) => void) {
this.connection.on('ExistingParticipants', callback)
}
setAttendanceUpdatedHandler(callback: (record: ClassroomAttendanceDto) => void) {
this.onAttendanceUpdate = callback
}
setParticipantJoinHandler(
callback: (userId: string, name: string, isTeacher: boolean, isActive: boolean) => void,
) {
this.onParticipantJoined = callback
}
setParticipantLeaveHandler(
callback: (payload: { userId: string; sessionId: string; userName: string }) => void,
) {
this.onParticipantLeft = callback
}
setChatMessageReceivedHandler(callback: (message: ClassroomChatDto) => void) {
this.onChatMessage = callback
}
setParticipantMutedHandler(callback: (userId: string, isMuted: boolean) => void) {
this.onParticipantMuted = callback
}
setHandRaiseReceivedHandler(callback: (studentId: string) => void) {
this.onHandRaiseReceived = callback
}
setHandRaiseDismissedHandler(callback: (studentId: string) => void) {
this.onHandRaiseDismissed = callback
}
setOfferReceivedHandler(
callback: (fromUserId: string, offer: RTCSessionDescriptionInit) => void,
) {
this.onOfferReceived = callback
}
setAnswerReceivedHandler(
callback: (fromUserId: string, answer: RTCSessionDescriptionInit) => void,
) {
this.onAnswerReceived = callback
}
setIceCandidateReceivedHandler(
callback: (fromUserId: string, candidate: RTCIceCandidateInit) => void,
) {
this.onIceCandidateReceived = callback
}
async disconnect(): Promise<void> {
if (this.isConnected && this.currentSessionId) {
try {
await this.connection.invoke('LeaveClass', this.currentSessionId)
} catch {
toast.push(<Notification title="⚠️ Bağlantı koparılırken hata" type="warning" />, {
placement: 'top-end',
})
}
}
if (this.connection) {
await this.connection.stop()
}
this.isConnected = false
this.currentSessionId = undefined
}
getConnectionState(): boolean {
return this.isConnected
}
setForceCleanupHandler(callback: () => void) {
this.onForceCleanup = callback
}
}

View file

@ -1,358 +0,0 @@
import { toast } from '@/components/ui'
import Notification from '@/components/ui/Notification'
export class WebRTCService {
private peerConnections: Map<string, RTCPeerConnection> = new Map()
private retryCounts: Map<string, number> = new Map()
private maxRetries = 3
private signalRService: any
private sessionId: string = ''
private localStream: MediaStream | null = null
private onRemoteStream?: (userId: string, stream: MediaStream) => void
private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void
private candidateBuffer: Map<string, RTCIceCandidateInit[]> = new Map()
private rtcConfiguration: RTCConfiguration = {
iceServers: [
{
urls: [
'stun:turn.sozsoft.com:3478',
'turn:turn.sozsoft.com:3478?transport=udp',
'turn:turn.sozsoft.com:3478?transport=tcp',
'turns:turn.sozsoft.com:5349?transport=tcp',
],
username: 'webrtc',
credential: 'strongpassword123',
},
],
}
async initializeLocalStream(enableAudio: boolean, enableVideo: boolean): Promise<MediaStream> {
try {
this.localStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
},
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
})
this.localStream.getAudioTracks().forEach((track) => (track.enabled = enableAudio))
this.localStream.getVideoTracks().forEach((track) => (track.enabled = enableVideo))
return this.localStream
} catch {
toast.push(
<Notification
title="❌ Kamera/Mikrofon erişilemedi. Tarayıcı ayarlarınızı veya izinleri kontrol edin."
type="danger"
/>,
{ placement: 'top-end' },
)
throw new Error('Media devices access failed')
}
}
async createPeerConnection(userId: string): Promise<RTCPeerConnection> {
const peerConnection = new RTCPeerConnection(this.rtcConfiguration)
this.peerConnections.set(userId, peerConnection)
this.retryCounts.set(userId, 0)
if (this.localStream) {
this.localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, this.localStream!)
})
}
peerConnection.ontrack = (event) => {
const [remoteStream] = event.streams
this.onRemoteStream?.(userId, remoteStream)
}
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.onIceCandidate?.(userId, event.candidate)
}
}
peerConnection.onconnectionstatechange = async () => {
const state = peerConnection.connectionState
if (state === 'closed') {
this.closePeerConnection(userId)
}
if (state === 'failed') {
let retries = this.retryCounts.get(userId) ?? 0
if (retries < this.maxRetries) {
toast.push(
<Notification
title={`⚠️ Bağlantı başarısız, yeniden deneniyor (${retries + 1}/${this.maxRetries})`}
type="warning"
/>,
)
this.retryCounts.set(userId, retries + 1)
await this.restartIce(peerConnection, userId)
} else {
toast.push(
<Notification
title={`❌ Bağlantı kurulamadı (${this.maxRetries} deneme başarısız).`}
type="danger"
/>,
{ placement: 'top-end' },
)
this.closePeerConnection(userId)
}
}
}
if (this.candidateBuffer.has(userId)) {
for (const cand of this.candidateBuffer.get(userId)!) {
try {
await peerConnection.addIceCandidate(cand)
} catch {
toast.push(
<Notification
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
type="warning"
/>,
)
}
}
this.candidateBuffer.delete(userId)
}
return peerConnection
}
setSignalRService(signalRService: any, sessionId: string) {
this.signalRService = signalRService
this.sessionId = sessionId
}
setIceCandidateHandler(callback: (userId: string, candidate: RTCIceCandidateInit) => void) {
this.onIceCandidate = callback
}
async createOffer(userId: string): Promise<RTCSessionDescriptionInit> {
const pc = this.peerConnections.get(userId)
if (!pc) throw new Error('Peer connection not found')
try {
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
return offer
} catch {
toast.push(<Notification title="❌ Offer oluşturulamadı" type="danger" />, {
placement: 'top-end',
})
throw new Error('Offer creation failed')
}
}
async createAnswer(
userId: string,
offer: RTCSessionDescriptionInit,
): Promise<RTCSessionDescriptionInit> {
const pc = this.peerConnections.get(userId)
if (!pc) throw new Error('Peer connection not found')
try {
await pc.setRemoteDescription(offer)
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
return answer
} catch {
toast.push(<Notification title="❌ Answer oluşturulamadı" type="danger" />, {
placement: 'top-end',
})
throw new Error('Answer creation failed')
}
}
async handleAnswer(userId: string, answer: RTCSessionDescriptionInit): Promise<void> {
const peerConnection = this.peerConnections.get(userId)
if (!peerConnection) throw new Error('Peer connection not found')
await peerConnection.setRemoteDescription(answer)
}
async addIceCandidate(userId: string, candidate: RTCIceCandidateInit): Promise<void> {
const pc = this.peerConnections.get(userId)
if (!pc) {
if (!this.candidateBuffer.has(userId)) {
this.candidateBuffer.set(userId, [])
}
this.candidateBuffer.get(userId)!.push(candidate)
return
}
if (pc.signalingState === 'stable' || pc.signalingState === 'have-remote-offer') {
try {
await pc.addIceCandidate(candidate)
} catch {
toast.push(
<Notification
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
type="warning"
/>,
)
}
} else {
if (!this.candidateBuffer.has(userId)) {
this.candidateBuffer.set(userId, [])
}
this.candidateBuffer.get(userId)!.push(candidate)
}
}
onRemoteStreamReceived(callback: (userId: string, stream: MediaStream) => void) {
this.onRemoteStream = callback
}
async toggleVideo(enabled: boolean): Promise<void> {
if (!this.localStream) return
let videoTrack = this.localStream.getVideoTracks()[0]
if (videoTrack) {
videoTrack.enabled = enabled
} else if (enabled) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
const newTrack = stream.getVideoTracks()[0]
if (newTrack) {
this.localStream!.addTrack(newTrack)
this.peerConnections.forEach((pc) => {
const sender = pc.getSenders().find((s) => s.track?.kind === newTrack.kind)
if (sender) {
sender.replaceTrack(newTrack)
} else {
pc.addTrack(newTrack, this.localStream!)
}
})
}
} catch {
toast.push(<Notification title="❌ Kamera açılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
}
async toggleAudio(enabled: boolean): Promise<void> {
if (!this.localStream) return
let audioTrack = this.localStream.getAudioTracks()[0]
if (audioTrack) {
audioTrack.enabled = enabled
} else if (enabled) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
const newTrack = stream.getAudioTracks()[0]
if (newTrack) {
this.localStream!.addTrack(newTrack)
this.peerConnections.forEach((pc) => {
const sender = pc.getSenders().find((s) => s.track?.kind === newTrack.kind)
if (sender) {
sender.replaceTrack(newTrack)
} else {
pc.addTrack(newTrack, this.localStream!)
}
})
}
} catch {
toast.push(<Notification title="❌ Mikrofon açılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
}
getLocalStream(): MediaStream | null {
return this.localStream
}
private async restartIce(peerConnection: RTCPeerConnection, userId: string) {
try {
const offer = await peerConnection.createOffer({ iceRestart: true })
await peerConnection.setLocalDescription(offer)
if (this.signalRService) {
await this.signalRService.sendOffer(this.sessionId, userId, offer)
} else {
toast.push(<Notification title="⚠️ Tekrar bağlanma başarısız" type="warning" />, {
placement: 'top-end',
})
}
} catch {
toast.push(<Notification title="❌ ICE restart başarısız" type="danger" />, {
placement: 'top-end',
})
}
}
closePeerConnection(userId: string): void {
const peerConnection = this.peerConnections.get(userId)
if (peerConnection) {
peerConnection.getSenders().forEach((sender) => sender.track?.stop())
peerConnection.close()
this.peerConnections.delete(userId)
this.retryCounts.delete(userId)
}
}
getPeerConnection(userId: string): RTCPeerConnection | undefined {
return this.peerConnections.get(userId)
}
closeAllConnections(): void {
this.peerConnections.forEach((pc) => {
pc.getSenders().forEach((sender) => sender.track?.stop())
pc.close()
})
this.peerConnections.clear()
if (this.localStream) {
this.localStream.getTracks().forEach((track) => track.stop())
this.localStream = null
}
}
addStreamToPeers(stream: MediaStream) {
this.peerConnections.forEach((pc) => {
stream.getTracks().forEach((track) => {
const alreadyHas = pc.getSenders().some((s) => s.track?.id === track.id)
if (!alreadyHas) {
pc.addTrack(track, stream)
track.onended = () => {
this.removeTrackFromPeers(track)
}
}
})
})
}
removeTrackFromPeers(track: MediaStreamTrack) {
this.peerConnections.forEach((pc) => {
pc.getSenders().forEach((sender) => {
if (sender.track === track) {
try {
pc.removeTrack(sender)
} catch {
toast.push(<Notification title="⚠️ Track silinemedi" type="warning" />, {
placement: 'top-end',
})
}
if (sender.track?.readyState !== 'ended') {
sender.track?.stop()
}
}
})
})
}
}

View file

@ -7,7 +7,7 @@ import {
UserClaimModel,
UserInfoViewModel,
} from '@/proxy/admin/models'
import { ListResultDto } from '../proxy'
import { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '../proxy'
import { AuditLogDto } from '../proxy/auditLog/audit-log'
import apiService from './api.service'
@ -74,6 +74,13 @@ export const getAuditLogs = (id: string) =>
url: `/api/app/audit-log/${id}`,
})
export const getAuditLogList = (input: PagedAndSortedResultRequestDto) =>
apiService.fetchData<PagedResultDto<AuditLogDto>, PagedAndSortedResultRequestDto>({
method: 'GET',
url: '/api/app/audit-log',
params: input,
})
export const postClaimUser = (input: UserClaimModel) =>
apiService.fetchData({
method: 'POST',

View file

@ -0,0 +1,21 @@
import apiService from './api.service'
export interface SetupStatusDto {
dbExists: boolean
error?: string
}
export const getSetupStatus = () =>
apiService.fetchData<SetupStatusDto>({
method: 'GET',
url: '/api/setup/status',
})
/**
* Returns the SSE URL for migration streaming.
* Usage: new EventSource(getMigrateUrl())
*/
export const getMigrateUrl = (): string => {
const base = import.meta.env.VITE_API_URL ?? ''
return `${base}/api/setup/migrate`
}

View file

@ -4,12 +4,16 @@ import { Injections } from './store'
import { GridOptionsEditDto } from '../proxy/form/models'
import setNull from '../utils/setNull'
import { ListState } from '@/proxy/admin/list-form/models'
import { Message } from '@/proxy/ai/models'
export interface AdminStoreModel {
lists: {
values: GridOptionsEditDto | undefined
states: ListState[]
}
messages: {
aiPosts: Message[]
}
}
export interface AdminStoreActions {
@ -23,6 +27,10 @@ export interface AdminStoreActions {
>
setStates: Action<AdminStoreModel['lists'], ListState>
}
messages: {
addAiPost: Action<AdminStoreModel['messages'], Message>
setAiPosts: Action<AdminStoreModel['messages'], Message[]>
}
}
export type AdminModel = AdminStoreModel & AdminStoreActions
@ -32,6 +40,9 @@ const initialState: AdminStoreModel = {
values: undefined,
states: [],
},
messages: {
aiPosts: [],
},
}
export const adminModel: AdminModel = {
@ -62,4 +73,14 @@ export const adminModel: AdminModel = {
actions.setListFormValues(result.data)
}),
},
messages: {
...initialState.messages,
addAiPost: action((state, payload) => {
state.aiPosts = [...state.aiPosts, payload]
}),
setAiPosts: action((state, payload) => {
state.aiPosts = payload
}),
},
}

View file

@ -1,24 +1,6 @@
import type { Action } from 'easy-peasy'
import { action } from 'easy-peasy'
type ChatType = 'chat' | 'query' | 'analyze'
interface BaseContent {
type: ChatType
question: string
sql: string | null
answer: string | any[]
chart?: string
error?: string
}
type MessageContent = string | BaseContent
export interface Message {
role: 'user' | 'assistant'
content: MessageContent
}
export interface StoreError {
id: string
title: string
@ -31,12 +13,11 @@ export interface BaseStoreModel {
common: {
currentRouteKey: string
tabHasFocus: boolean
setupMode: boolean /** Veritabanı mevcut değilse true — setup sayfasına yönlendirme için */
}
messages: {
errors: StoreError[]
// success: string[]
warning: string[]
aiPosts: Message[]
}
}
@ -44,27 +25,25 @@ export interface BaseStoreActions {
common: {
setCurrentRouteKey: Action<BaseStoreModel['common'], string>
setTabHasFocus: Action<BaseStoreModel['common'], boolean>
setSetupMode: Action<BaseStoreModel['common'], boolean>
}
messages: {
addError: Action<BaseStoreModel['messages'], StoreError>
removeError: Action<BaseStoreModel['messages'], string>
// setSuccess: Action<BaseStoreModel, string>
setWarning: Action<BaseStoreModel['messages'], string>
addAiPost: Action<BaseStoreModel['messages'], Message>
setAiPosts: Action<BaseStoreModel['messages'], Message[]>
}
}
export type BaseModel = BaseStoreModel & BaseStoreActions
const initialState: BaseStoreModel = {
common: { currentRouteKey: '', tabHasFocus: false },
common: { currentRouteKey: '', tabHasFocus: false, setupMode: false },
messages: {
errors: [],
// success: [],
warning: [],
aiPosts: [],
},
}
}
export const baseModel: BaseModel = {
@ -76,6 +55,9 @@ export const baseModel: BaseModel = {
setTabHasFocus: action((state, payload) => {
state.tabHasFocus = payload
}),
setSetupMode: action((state, payload) => {
state.setupMode = payload
}),
},
messages: {
...initialState.messages,
@ -92,12 +74,5 @@ export const baseModel: BaseModel = {
state.warning = []
}
}),
addAiPost: action((state, payload) => {
state.aiPosts = [...state.aiPosts, payload]
}),
setAiPosts: action((state, payload) => {
state.aiPosts = payload
}),
},
}

View file

@ -1,16 +1,8 @@
import type { Action } from 'easy-peasy'
import { action } from 'easy-peasy'
import { themeConfig } from '../proxy/theme/theme.config'
import { availableNavColorLayouts, themeConfig } from '../proxy/theme/theme.config'
import {
LAYOUT_TYPE_CLASSIC,
LAYOUT_TYPE_DECKED,
LAYOUT_TYPE_MODERN,
LAYOUT_TYPE_STACKED_SIDE,
MODE_DARK,
MODE_LIGHT,
NAV_MODE_DARK,
NAV_MODE_LIGHT,
NAV_MODE_THEMED,
NAV_MODE_TRANSPARENT,
} from '../constants/theme.constant'
import { Direction, Mode, NavMode } from '../proxy/theme/models'
@ -29,6 +21,7 @@ export interface ThemeStoreModel {
type: string
sideNavCollapse: boolean
}
style: string
}
export interface ThemeStoreActions {
@ -37,34 +30,26 @@ export interface ThemeStoreActions {
setLayout: Action<ThemeStoreModel, string>
setPreviousLayout: Action<ThemeStoreModel, string>
setSideNavCollapse: Action<ThemeStoreModel, boolean>
setNavMode: Action<ThemeStoreModel, NavMode | 'default'>
setNavMode: Action<ThemeStoreModel, NavMode>
setPanelExpand: Action<ThemeStoreModel, boolean>
setThemeColor: Action<ThemeStoreModel, string>
setThemeColorLevel: Action<ThemeStoreModel, number>
setStyle: Action<ThemeStoreModel, string>
}
export type ThemeModel = ThemeStoreModel & ThemeStoreActions
const availableNavColorLayouts = [LAYOUT_TYPE_CLASSIC, LAYOUT_TYPE_STACKED_SIDE, LAYOUT_TYPE_DECKED]
const initialNavMode = () => {
if (themeConfig.layout.type === LAYOUT_TYPE_MODERN && themeConfig.navMode !== NAV_MODE_THEMED) {
return NAV_MODE_TRANSPARENT
}
return themeConfig.navMode
}
const initialState: ThemeStoreModel = {
themeColor: themeConfig.themeColor,
direction: themeConfig.direction,
mode: themeConfig.mode,
primaryColorLevel: themeConfig.primaryColorLevel,
panelExpand: themeConfig.panelExpand,
navMode: initialNavMode(),
navMode: NAV_MODE_TRANSPARENT,
layout: themeConfig.layout,
cardBordered: true,
controlSize: 'md',
style: themeConfig.style,
}
export const themeModel: ThemeModel = {
@ -73,32 +58,15 @@ export const themeModel: ThemeModel = {
state.direction = payload
}),
setMode: action((state, payload) => {
const availableColorNav = availableNavColorLayouts.includes(state.layout.type)
if (availableColorNav && payload === MODE_DARK && state.navMode !== NAV_MODE_THEMED) {
state.navMode = NAV_MODE_DARK
}
if (availableColorNav && payload === MODE_LIGHT && state.navMode !== NAV_MODE_THEMED) {
state.navMode = NAV_MODE_LIGHT
}
state.mode = payload
}),
setLayout: action((state, payload) => {
state.cardBordered = payload === LAYOUT_TYPE_MODERN
if (payload === LAYOUT_TYPE_MODERN) {
if (availableNavColorLayouts.includes(payload)) {
state.navMode = NAV_MODE_TRANSPARENT
}
const availableColorNav = availableNavColorLayouts.includes(payload)
if (availableColorNav && state.mode === MODE_LIGHT) {
state.navMode = NAV_MODE_LIGHT
}
if (availableColorNav && state.mode === MODE_DARK) {
state.navMode = NAV_MODE_DARK
}
state.layout.type = payload
}),
setPreviousLayout: action((state, payload) => {
@ -108,23 +76,7 @@ export const themeModel: ThemeModel = {
state.layout.sideNavCollapse = payload
}),
setNavMode: action((state, payload) => {
if (payload !== 'default') {
state.navMode = payload
} else {
if (state.layout.type === LAYOUT_TYPE_MODERN) {
state.navMode = NAV_MODE_TRANSPARENT
}
const availableColorNav = availableNavColorLayouts.includes(state.layout.type)
if (availableColorNav && state.mode === MODE_LIGHT) {
state.navMode = NAV_MODE_LIGHT
}
if (availableColorNav && state.mode === MODE_DARK) {
state.navMode = NAV_MODE_DARK
}
}
}),
setPanelExpand: action((state, payload) => {
state.panelExpand = payload
@ -135,4 +87,7 @@ export const themeModel: ThemeModel = {
setThemeColorLevel: action((state, payload) => {
state.primaryColorLevel = payload
}),
setStyle: action((state, payload) => {
state.style = payload
}),
}

View file

@ -4,6 +4,16 @@ import { FaUser } from 'react-icons/fa'
import navigationIcon from '@/proxy/menus/navigation-icon.config'
import { navigationTreeToFlat } from '@/utils/navigation'
const extractListFormCode = (path: string): string | null => {
const p = (path ?? '').toLowerCase()
// /admin/form/<code>/..., /admin/list/<code>/..., /admin/chart/<code>/..., /admin/pivot/<code>/...
let m = p.match(/\/admin\/(?:list|form|chart|pivot)\/([^/?#]+)/)
if (m?.[1]) return m[1]
return null
}
export function useCurrentMenuIcon(className = 'w-6 h-6'): JSX.Element {
const mainMenu = useStoreState((state) => state.abpConfig.menu.mainMenu)
const location = useLocation()
@ -18,6 +28,12 @@ export function useCurrentMenuIcon(className = 'w-6 h-6'): JSX.Element {
// Exact match
if (currentPath.startsWith(menuPath)) return true
// Form/list/chart/pivot routes can include extra segments (e.g. /:id, /edit).
// Match by listFormCode extracted from both paths.
const currentCode = extractListFormCode(currentPath)
const menuCode = extractListFormCode(menuPath)
if (currentCode && menuCode && currentCode === menuCode) return true
// Extract the form code (e.g., "App.Definitions.Program" from path)
const menuFormCode = menuPath.split('/').pop() || ''

View file

@ -1,3 +1,4 @@
import { Button } from '@/components/ui'
import { APP_NAME } from '@/constants/app.constant'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -27,14 +28,15 @@ const NotFoundPage = () => {
{translate('::Public.notFound.message')}
</p>
<div className="flex items-center justify-center font-inter">
<button
<Button
variant='solid'
onClick={() =>
navigate(isAdminPath ? ROUTES_ENUM.protected.dashboard : ROUTES_ENUM.public.home)
}
className="px-6 py-3 bg-blue-500 rounded-xl shadow hover:bg-blue-600 transition"
>
{translate('::Public.notFound.button')}
</button>
</Button>
</div>
</div>
)

View file

@ -1,5 +1,7 @@
import AdaptableCard from '@/components/shared/AdaptableCard'
import Container from '@/components/shared/Container'
import Drawer from '@/components/ui/Drawer'
import Button from '@/components/ui/Button'
import NotificationChannels from '@/constants/notification-channel.enum'
import { NotificationDto } from '@/proxy/notification/models'
import { getList } from '@/services/notification.service'
@ -9,13 +11,14 @@ import { Dictionary } from 'lodash'
import forOwn from 'lodash/forOwn'
import groupBy from 'lodash/groupBy'
import has from 'lodash/has'
import isEmpty from 'lodash/isEmpty'
import merge from 'lodash/merge'
import { useEffect, useState } from 'react'
import Log from './components/Log'
import LogFilter from './components/LogFilter'
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
import { DIR_RTL } from '@/constants/theme.constant'
import { useStoreState } from '@/store'
const itemsPerPage = 10
@ -26,12 +29,13 @@ const ActivityLog = () => {
const [notifications, setNotifications] = useState<Dictionary<NotificationDto[]>>({})
const [page, setPage] = useState(0)
const [hasMore, setHasMore] = useState(false)
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false)
const direction = useStoreState((state) => state.theme.direction)
const [filter, setFilter] = useState<string[]>([
NotificationChannels.Desktop,
NotificationChannels.Mail,
NotificationChannels.Rocket,
NotificationChannels.Sms,
NotificationChannels.Telegram,
NotificationChannels.UiActivity,
NotificationChannels.UiToast,
NotificationChannels.WhatsApp,
@ -67,18 +71,16 @@ const ActivityLog = () => {
}
useEffect(() => {
if (isEmpty(notifications)) {
fetchData()
fetchData(page > 0)
}, [page, filter])
const handleFilterChange = (value: string[]) => {
setPage(0)
setNotifications({})
setHasMore(false)
setFilter(value)
setIsFilterDrawerOpen(false)
}
}, [])
useEffect(() => {
fetchData(true)
}, [page])
useEffect(() => {
fetchData()
}, [filter])
return (
<Container>
@ -88,19 +90,53 @@ const ActivityLog = () => {
defaultTitle={APP_NAME}
></Helmet>
<AdaptableCard>
<div className="grid lg:grid-cols-5 gap-8">
<div className="col-span-4">
<h3 className="mb-6">{translate('::Abp.Identity.ActivityLogs')}</h3>
<AdaptableCard className="overflow-hidden">
<div className="w-full">
<div className="mb-5 flex items-center justify-between gap-3">
<h3 className="text-xl font-semibold md:text-2xl">
{translate('::Abp.Identity.ActivityLogs')}
</h3>
<Button
className="lg:hidden"
size="sm"
variant="twoTone"
onClick={() => setIsFilterDrawerOpen(true)}
>
{translate('::Abp.Identity.ActivityLogs.Filters')}
</Button>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_340px]">
<div className="min-w-0 rounded-xl border border-gray-200 bg-white p-4 lg:p-6">
<Log
notifications={notifications}
isLoading={loading}
onLoadMore={() => setPage(page + 1)}
onLoadMore={() => setPage((prev) => prev + 1)}
loadable={hasMore}
></Log>
</div>
<LogFilter filter={filter} onFilterChange={(value: string[]) => setFilter(value)} />
<div className="hidden lg:block">
<LogFilter filter={filter} onFilterChange={handleFilterChange} useAffix />
</div>
</div>
</div>
<Drawer
title={translate('::Abp.Identity.ActivityLogs.Filters')}
isOpen={isFilterDrawerOpen}
width={340}
placement={direction === DIR_RTL ? 'right' : 'left'}
onClose={() => setIsFilterDrawerOpen(false)}
onRequestClose={() => setIsFilterDrawerOpen(false)}
>
<LogFilter
filter={filter}
onFilterChange={handleFilterChange}
useAffix={false}
className="border-none p-0"
/>
</Drawer>
</AdaptableCard>
</Container>
)

View file

@ -28,7 +28,7 @@ const Log = ({
return (
<Loading type="cover" loading={isLoading}>
<div className="max-w-[900px]">
<div className="w-full">
{keys(notifications).map((group) => (
<div key={group} className="mb-8">
<div className="mb-4 font-semibold uppercase">

View file

@ -15,7 +15,6 @@ const ticketCheckboxes = [
{ label: NotificationChannels.UiActivity, value: NotificationChannels.UiActivity },
{ label: NotificationChannels.UiToast, value: NotificationChannels.UiToast },
{ label: NotificationChannels.WhatsApp, value: NotificationChannels.WhatsApp },
{ label: NotificationChannels.Telegram, value: NotificationChannels.Telegram },
]
const CategoryTitle = ({ children, className }: CategoryTitleProps) => {
@ -34,16 +33,19 @@ const CategoryTitle = ({ children, className }: CategoryTitleProps) => {
const LogFilter = ({
filter,
onFilterChange,
useAffix = true,
className,
}: {
filter: string[]
onFilterChange: (value: string[]) => void
useAffix?: boolean
className?: string
}) => {
const { translate } = useLocalization()
return (
<div>
<Affix className="hidden lg:block" offset={80}>
<h5 className="mb-4">{translate('::Abp.Identity.ActivityLogs.Filters')}</h5>
const content = (
<div className={classNames('rounded-xl border border-gray-200 bg-white p-4', className)}>
<h5 className="mb-4 text-base font-semibold">{translate('::Abp.Identity.ActivityLogs.Filters')}</h5>
<Checkbox.Group
vertical
value={filter}
@ -51,18 +53,23 @@ const LogFilter = ({
onFilterChange(value as string[])
}}
>
<CategoryTitle className="mb-3">
<CategoryTitle className="mb-3 text-gray-500">
{translate('::Abp.Identity.ActivityLogs.Channels')}
</CategoryTitle>
{ticketCheckboxes.map((checkbox) => (
<Checkbox key={checkbox.value} className="mb-4" value={checkbox.value}>
<Checkbox key={checkbox.value} className="mb-3" value={checkbox.value}>
{checkbox.label}
</Checkbox>
))}
</Checkbox.Group>
</Affix>
</div>
)
if (useAffix) {
return <Affix offset={80}>{content}</Affix>
}
return content
}
export default LogFilter

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { Helmet } from 'react-helmet'
import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui'
import { useStoreState } from '@/store'
import {
FaFolder,
FaCloudUploadAlt,
@ -43,6 +44,10 @@ import { APP_NAME } from '@/constants/app.constant'
const FileManager = () => {
const { translate } = useLocalization()
const authTenantId = useStoreState((state) => state.auth.tenant?.tenantId)
const authTenantName = useStoreState((state) => state.auth.tenant?.tenantName)
const isHostContext = !authTenantId
// State
const [loading, setLoading] = useState(true)
const [items, setItems] = useState<FileItemType[]>([])
@ -71,7 +76,7 @@ const FileManager = () => {
const [tenants, setTenants] = useState<TenantDto[]>([])
const [tenantsLoading, setTenantsLoading] = useState(false)
const [selectedTenant, setSelectedTenant] = useState<{ id: string; name: string } | undefined>(
undefined,
authTenantId ? { id: authTenantId, name: authTenantName || '' } : undefined,
)
// Tracks mid-flight tenant change so the fetch effect doesn't fire with a stale folderId
const pendingTenantChange = useRef(false)
@ -96,8 +101,20 @@ const FileManager = () => {
}, [])
useEffect(() => {
if (isHostContext) {
fetchTenants()
}, [fetchTenants])
}
}, [fetchTenants, isHostContext])
// If user is in a tenant context, lock selection to that tenant.
useEffect(() => {
if (!authTenantId) return
setSelectedTenant((prev) => {
if (prev?.id === authTenantId) return prev
return { id: authTenantId, name: authTenantName || prev?.name || '' }
})
}, [authTenantId, authTenantName])
// Reset navigation when tenant changes
useEffect(() => {
@ -124,20 +141,6 @@ const FileManager = () => {
}
})
// console.log('Fetched items:', protectedItems)
// console.log(
// 'Protected folders check:',
// protectedItems.filter((item) => item.isReadOnly),
// )
// console.log(
// 'Folders with childCount:',
// protectedItems.filter((item) => item.type === 'folder').map(item => ({
// name: item.name,
// childCount: item.childCount,
// hasChildCount: 'childCount' in item,
// type: typeof item.childCount
// }))
// )
setItems(protectedItems)
} catch (error) {
console.error('Failed to fetch items:', error)
@ -668,14 +671,14 @@ const FileManager = () => {
await fileManagementService.copyItems(itemIds, currentFolderId, selectedTenant?.id)
await fetchItems(currentFolderId)
toast.push(
<Notification title="Success" type="success">
<Notification title={translate('::App.Platform.Success')} type="success">
{itemIds.length} item(s) copied successfully
</Notification>,
)
} catch (error) {
console.error('Copy failed:', error)
toast.push(
<Notification title="Error" type="danger">
<Notification title={translate('::App.Platform.Error')} type="danger">
Failed to copy items
</Notification>,
)
@ -701,14 +704,14 @@ const FileManager = () => {
localStorage.removeItem('fileManager_clipboard')
setHasClipboardData(false)
toast.push(
<Notification title="Success" type="success">
<Notification title={translate('::App.Platform.Success')} type="success">
{itemIds.length} item(s) moved successfully
</Notification>,
)
} catch (error) {
console.error('Move failed:', error)
toast.push(
<Notification title="Error" type="danger">
<Notification title={translate('::App.Platform.Error')} type="danger">
Failed to move items
</Notification>,
)
@ -742,6 +745,7 @@ const FileManager = () => {
{/* Tenant Selector Row */}
<div className="flex items-center gap-2">
<FaBuilding className="text-gray-500 flex-shrink-0" />
{isHostContext ? (
<Select
size="xs"
isLoading={tenantsLoading}
@ -765,6 +769,14 @@ const FileManager = () => {
}
}}
/>
) : (
<div
className="text-sm font-medium text-gray-700 dark:text-gray-200 truncate max-w-[220px]"
title={authTenantName || selectedTenant?.name || ''}
>
{authTenantName || selectedTenant?.name || ''}
</div>
)}
</div>
{/* File Operations */}

View file

@ -2,6 +2,7 @@ import { forwardRef } from 'react'
import classNames from 'classnames'
import { FaChevronRight, FaFolder, FaHome } from 'react-icons/fa'
import type { BreadcrumbItem } from '@/types/fileManagement'
import { Button } from '@/components/ui'
export interface BreadcrumbProps {
items: BreadcrumbItem[]
@ -17,7 +18,8 @@ const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>((props, ref) => {
{items.map((item, index) => (
<div key={item.path} className="flex items-center">
{index > 0 && <FaChevronRight className="mx-2 h-4 w-4 text-gray-400" />}
<button
<Button
size="xs"
onClick={() => onNavigate(item)}
className={classNames(
'flex items-center px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
@ -33,7 +35,7 @@ const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>((props, ref) => {
<FaFolder className="h-4 w-4 mr-1" />
)}
<span className="truncate max-w-32">{item.name}</span>
</button>
</Button>
</div>
))}
</div>

View file

@ -324,6 +324,8 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
const ImagePreview = ({ src, alt }: { src: string; alt: string }) => {
const [imageError, setImageError] = useState(false)
console.log('Rendering ImagePreview with src:', src) // Debug için
return (
<div className="w-full h-full bg-gray-100 dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden">
{!imageError ? (
@ -378,7 +380,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
<div className="w-8 h-8">
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
<ImagePreview
src={`/api/app/file-management/${item.id}/download-file`}
src={FILE_URL(item.path, item.tenantId)}
alt={item.name}
/>
) : (
@ -477,7 +479,7 @@ const FileItem = forwardRef<HTMLDivElement, FileItemProps>((props, ref) => {
{item.type === 'file' && item.mimeType?.startsWith('image/') ? (
<div className="w-16 h-16">
<ImagePreview
src={`/api/app/file-management/${item.id}/download-file`}
src={FILE_URL(item.path, item.tenantId)}
alt={item.name}
/>
</div>

View file

@ -137,6 +137,20 @@ const Wizard = () => {
// ── Editing Form Groups (Step 3) ──
const [editingGroups, setEditingGroups] = useState<WizardGroup[]>([])
// Audit columns that should not be selected by default
const AUDIT_COLUMNS = new Set([
'creationtime',
'creatorid',
'lastmodificationtime',
'lastmodifierid',
'isdeleted',
'deletiontime',
'deleterid',
])
const isAuditColumn = (columnName: string) =>
AUDIT_COLUMNS.has(columnName.toLowerCase())
const loadColumns = async (dsCode: string, schema: string, name: string) => {
if (!dsCode || !name) {
setSelectCommandColumns([])
@ -149,7 +163,8 @@ const Wizard = () => {
const res = await sqlObjectManagerService.getTableColumns(dsCode, schema, name)
const cols = res.data ?? []
setSelectCommandColumns(cols)
setSelectedColumns(new Set(cols.map((c) => c.columnName)))
const selectableColumns = cols.filter((c) => !isAuditColumn(c.columnName))
setSelectedColumns(new Set(selectableColumns.map((c) => c.columnName)))
setEditingGroups([])
// Auto-select first column as key field
if (cols.length > 0) {
@ -176,7 +191,15 @@ const Wizard = () => {
})
const toggleAllColumns = (all: boolean) =>
setSelectedColumns(all ? new Set(selectCommandColumns.map((c) => c.columnName)) : new Set())
setSelectedColumns(
all
? new Set(
selectCommandColumns
.filter((c) => !isAuditColumn(c.columnName))
.map((c) => c.columnName),
)
: new Set(),
)
const getDataSourceList = async () => {
setIsLoadingDataSource(true)
@ -235,11 +258,26 @@ const Wizard = () => {
return sanitized ? `App.Wizard.${sanitized}` : ''
}
const toSpacedLabel = (value: string) =>
value
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
.trim()
const handleWizardNameChange = (name: string) => {
formikRef.current?.setFieldValue('wizardName', name)
const spacedLabel = toSpacedLabel(name)
const derived = deriveListFormCode(name)
formikRef.current?.setFieldValue('wizardName', name)
formikRef.current?.setFieldValue('listFormCode', derived)
formikRef.current?.setFieldValue('menuCode', derived)
formikRef.current?.setFieldValue('languageTextMenuEn', spacedLabel)
formikRef.current?.setFieldValue('languageTextMenuTr', spacedLabel)
formikRef.current?.setFieldValue('languageTextTitleEn', spacedLabel)
formikRef.current?.setFieldValue('languageTextTitleTr', spacedLabel)
formikRef.current?.setFieldValue('languageTextDescEn', spacedLabel)
formikRef.current?.setFieldValue('languageTextDescTr', spacedLabel)
}
const handleMenuParentChange = (code: string) => {
@ -377,7 +415,7 @@ const Wizard = () => {
<Steps.Item
title={translate('::ListForms.Wizard.ListFormFields') || 'List Form Fields'}
/>
<Steps.Item title={translate('::ListForms.Wizard.Deploy') || 'Deploy'} />
<Steps.Item title={translate('::App.Platform.Deploy') || 'Deploy'} />
</Steps>
</div>

View file

@ -6,7 +6,7 @@ import navigationIcon from '@/proxy/menus/navigation-icon.config'
import { MenuItem } from '@/proxy/menus/menu'
import { MenuService } from '@/services/menu.service'
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import {
FaArrowRight,
@ -411,7 +411,15 @@ const WizardStep1 = ({
}: WizardStep1Props) => {
const [menuDialogOpen, setMenuDialogOpen] = useState(false)
const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
const [menuDialogInitialOrder, setMenuDialogInitialOrder] = useState(999)
const menuDialogInitialOrder = useMemo(() => {
const maxOrder = rawMenuItems.reduce((max, item) => {
const order = typeof item.order === 'number' ? item.order : 0
return order > max ? order : max
}, 0)
return maxOrder + 100
}, [rawMenuItems])
const step1Missing = [
!wizardName && translate('::ListForms.Wizard.Step1.WizardName'),
@ -419,6 +427,7 @@ const WizardStep1 = ({
!values.permissionGroupName && translate('::ListForms.Wizard.Step1.PermissionGroupName'),
!values.languageTextMenuEn && translate('::ListForms.Wizard.Step4.MenuEn'),
!values.languageTextMenuTr && translate('::ListForms.Wizard.Step4.MenuTr'),
!values.menuIcon && translate('::ListForms.Wizard.Step4.MenuIcon'),
].filter(Boolean) as string[]
const step1CanGo = step1Missing.length === 0
@ -464,8 +473,6 @@ const WizardStep1 = ({
? findRootCode(rawMenuItems, values.menuParentCode)
: '',
)
const selectedItem = rawMenuItems.find((i) => i.code === values.menuParentCode)
setMenuDialogInitialOrder(selectedItem?.order ?? 999)
setMenuDialogOpen(true)
}}
className="flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-green-500 text-white hover:bg-green-600"

View file

@ -62,7 +62,7 @@ const WizardStep2 = ({
onNext,
}: WizardStep2Props) => {
const step2Missing = [
!values.listFormCode && translate('::ListForms.Wizard.Step2.ListFormCode'),
!values.listFormCode && translate('::App.Listform.ListformField.ListFormCode'),
!values.dataSourceCode && translate('::ListForms.Wizard.Step4.DataSource'),
!values.selectCommand && translate('::ListForms.Wizard.Step2.SelectCommand'),
!values.keyFieldName && translate('::ListForms.Wizard.Step4.KeyField'),
@ -80,7 +80,7 @@ const WizardStep2 = ({
{/* ListForm Code + Data Source */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-6">
<FormItem
label={translate('::ListForms.Wizard.Step2.ListFormCode')}
label={translate('::App.Listform.ListformField.ListFormCode')}
invalid={!!(errors.listFormCode && touched.listFormCode)}
errorMessage={errors.listFormCode}
asterisk={true}
@ -102,7 +102,7 @@ const WizardStep2 = ({
</FormItem>
<FormItem
label={translate('::ListForms.Wizard.Step2.DataSourceCode')}
label={translate('::App.Listform.ListformField.DataSourceCode')}
asterisk={true}
invalid={!!(errors.dataSourceCode && touched.dataSourceCode)}
errorMessage={errors.dataSourceCode}
@ -112,7 +112,7 @@ const WizardStep2 = ({
<Select
field={field}
form={form}
placeholder={translate('::ListForms.Wizard.Step2.DataSourceCode')}
placeholder={translate('::App.Listform.ListformField.DataSourceCode')}
isClearable={true}
isLoading={isLoadingDataSource}
options={dataSourceList}
@ -137,7 +137,7 @@ const WizardStep2 = ({
{isDataSourceNew && (
<FormItem
label={translate('::ListForms.Wizard.Step2.ConnectionString')}
label={translate('::App.Listform.ListformField.ConnectionString')}
invalid={!!(errors.dataSourceConnectionString && touched.dataSourceConnectionString)}
errorMessage={errors.dataSourceConnectionString}
>
@ -145,7 +145,7 @@ const WizardStep2 = ({
type="text"
autoComplete="off"
name="dataSourceConnectionString"
placeholder={translate('::ListForms.Wizard.Step2.ConnectionString')}
placeholder={translate('::App.Listform.ListformField.ConnectionString')}
component={Input}
/>
</FormItem>
@ -195,7 +195,7 @@ const WizardStep2 = ({
})),
},
{
label: translate('::ListForms.Wizard.Step2.Views') || 'Views',
label: translate('::App.Platform.Views') || 'Views',
options: dbObjects.views.map((v) => ({
label: v.objectName,
value: v.objectName,
@ -362,7 +362,7 @@ const WizardStep2 = ({
</FormItem>
<FormItem
label={translate('::ListForms.Wizard.Step2.AllowEditing')}
label={translate('::ListForms.ListFormEdit.AllowEditing')}
invalid={!!(errors.allowEditing && touched.allowEditing)}
errorMessage={errors.allowEditing}
>
@ -473,23 +473,23 @@ const WizardStep2 = ({
extra={
selectCommandColumns.length > 0 ? (
<div className="flex items-center gap-2 ml-3">
<button
type="button"
<Button
variant='solid'
onClick={() => onToggleAllColumns(true)}
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600"
>
{translate('::ListForms.Wizard.Step2.SelectAll') || 'Tümünü Seç'}
</button>
<button
type="button"
</Button>
<Button
variant='default'
onClick={() => onToggleAllColumns(false)}
className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
>
{translate('::ListForms.Wizard.Step2.ClearAll') || 'Tümünü Kaldır'}
</button>
</Button>
<span className="text-xs text-gray-400">
{selectedColumns.size}/{selectCommandColumns.length}{' '}
{translate('::ListForms.Wizard.Step4.StatColumn')}
{translate('::App.Listform.ListformField.Column')}
</span>
</div>
) : null

View file

@ -84,7 +84,8 @@ const formatLabel = (text: string) => {
.join(" ");
};
function newGroupItem(colName: string, sqlType = ''): WizardGroupItem {
function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupItem {
const sqlType = meta?.dataType ?? ''
return {
id: `${colName}_${Date.now()}`,
dataField: colName,
@ -92,7 +93,7 @@ function newGroupItem(colName: string, sqlType = ''): WizardGroupItem {
editorOptions: '',
editorScript: '',
colSpan: 1,
isRequired: false,
isRequired: meta?.isNullable === false,
turkishCaption: formatLabel(colName),
englishCaption: formatLabel(colName),
}
@ -467,8 +468,7 @@ const WizardStep3 = ({
const availableColumns = [...selectedColumns].filter((c) => !placedColumns.has(c))
// ── Helpers ───────────────────────────────────────────────────────────────
const colMeta = (name: string) =>
selectCommandColumns.find((c) => c.columnName === name)?.dataType ?? ''
const colMeta = (name: string) => selectCommandColumns.find((c) => c.columnName === name)
const addColumnToGroup = (colName: string, targetGroupId: string) => {
onGroupsChange(
@ -679,7 +679,7 @@ const WizardStep3 = ({
<div className="sticky top-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
{translate('::ListForms.Wizard.Step4.StatColumn')}
{translate('::App.Listform.ListformField.Column')}
</span>
<span className="text-xs text-gray-400">
{availableColumns.length}/{selectedColumns.size}

View file

@ -207,7 +207,7 @@ const WizardStep4 = ({
<Row label={translate('::ListForms.Wizard.Step4.MenuCode')} value={values.menuCode} />
<Row label={translate('::ListForms.Wizard.Step4.MenuParent')} value={values.menuParentCode} />
<Row label={translate('::ListForms.Wizard.Step4.PermissionGroup')} value={values.permissionGroupName} />
<Row label={translate('::ListForms.Wizard.Step4.Icon')} value={values.menuIcon} />
<Row label={translate('::App.Listform.ListformField.Icon')} value={values.menuIcon} />
<Row label={translate('::ListForms.Wizard.Step4.MenuTr')} value={values.languageTextMenuTr} />
<Row label={translate('::ListForms.Wizard.Step4.MenuEn')} value={values.languageTextMenuEn} />
<Row label={translate('::ListForms.Wizard.Step4.MenuParentTr')} value={values.languageTextMenuParentTr} />
@ -221,7 +221,7 @@ const WizardStep4 = ({
<Row label={translate('::ListForms.Wizard.Step4.DescTr')} value={values.languageTextDescTr} />
<Row label={translate('::ListForms.Wizard.Step4.DescEn')} value={values.languageTextDescEn} />
<Row label={translate('::ListForms.Wizard.Step4.DataSource')} value={values.dataSourceCode} />
<Row label={translate('::ListForms.Wizard.Step4.ConnectionString')} value={values.dataSourceConnectionString} />
<Row label={translate('::App.Listform.ListformField.ConnectionString')} value={values.dataSourceConnectionString} />
<Row
label={translate('::ListForms.Wizard.Step4.CommandType')}
value={
@ -266,7 +266,7 @@ const WizardStep4 = ({
<Section
key={g.id}
title={g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
badge={`${g.items.length} ${translate('::ListForms.Wizard.Step4.StatField')} · ${g.colCount} ${translate('::ListForms.Wizard.Step4.StatColumn')}`}
badge={`${g.items.length} ${translate('::ListForms.Wizard.Step4.StatField')} · ${g.colCount} ${translate('::App.Listform.ListformField.Column')}`}
defaultOpen={false}
>
<div className="grid grid-cols-2 gap-2">
@ -307,7 +307,7 @@ const WizardStep4 = ({
{[
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
{ label: translate('::ListForms.Wizard.Step4.StatColumn'), value: selectedColumns.size },
{ label: translate('::App.Listform.ListformField.Column'), value: selectedColumns.size },
].map((s) => (
<div
key={s.label}

View file

@ -58,7 +58,7 @@ function FormTabCommands() {
<Th>{translate('::ListForms.ListFormEdit.CommandPosition')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CommandText')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CommandHint')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CommandIcon')}</Th>
<Th>{translate('::App.Listform.ListformField.Icon')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CommandAuthorizationType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CommandUrlTarget')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CommandUrl')}</Th>

View file

@ -104,12 +104,12 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
const table = dbObjects.tables.find((t) => t.tableName === cmd)
if (table) { loadColumns(dsCode, table.schemaName, table.tableName); return }
const view = dbObjects.views.find((v) => v.viewName === cmd)
if (view) { loadColumns(dsCode, view.schemaName, view.viewName); return }
const fn = dbObjects.functions.find((f) => f.functionName === cmd)
if (fn) { loadColumns(dsCode, fn.schemaName, fn.functionName); return }
const sp = dbObjects.storedProcedures.find((p) => p.procedureName === cmd)
if (sp) { loadColumns(dsCode, sp.schemaName, sp.procedureName); return }
const view = dbObjects.views.find((v) => v.objectName === cmd)
if (view) { loadColumns(dsCode, view.schemaName, view.objectName); return }
const fn = dbObjects.functions.find((f) => f.objectName === cmd)
if (fn) { loadColumns(dsCode, fn.schemaName, fn.objectName); return }
const sp = dbObjects.storedProcedures.find((p) => p.objectName === cmd)
if (sp) { loadColumns(dsCode, sp.schemaName, sp.objectName); return }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dbObjects])
@ -167,7 +167,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceCode')}
label={translate('::App.Listform.ListformField.DataSourceCode')}
invalid={errors.dataSourceCode && touched.dataSourceCode}
errorMessage={errors.dataSourceCode}
>
@ -175,7 +175,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
type="text"
autoComplete="off"
name="dataSourceCode"
placeholder={translate('::ListForms.ListFormEdit.DatabaseDataSourceCode')}
placeholder={translate('::App.Listform.ListformField.DataSourceCode')}
>
{({ field, form }: FieldProps<DataSourceTypeEnum>) => (
<Select
@ -232,31 +232,31 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
{
label: 'Views',
options: dbObjects.views.map((v) => ({
label: v.viewName,
value: v.viewName,
label: v.objectName,
value: v.objectName,
__type: SelectCommandTypeEnum.View,
__schema: v.schemaName,
__rawName: v.viewName,
__rawName: v.objectName,
})),
},
{
label: 'Functions',
options: dbObjects.functions.map((f) => ({
label: f.functionName,
value: f.functionName,
label: f.objectName,
value: f.objectName,
__type: SelectCommandTypeEnum.TableValuedFunction,
__schema: f.schemaName,
__rawName: f.functionName,
__rawName: f.objectName,
})),
},
{
label: 'Stored Procedures',
options: dbObjects.storedProcedures.map((p) => ({
label: p.procedureName,
value: p.procedureName,
label: p.objectName,
value: p.objectName,
__type: SelectCommandTypeEnum.StoredProcedure,
__schema: p.schemaName,
__rawName: p.procedureName,
__rawName: p.objectName,
})),
},
]
@ -312,7 +312,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceTableName')}
label={translate('::App.Listform.ListformField.TableName')}
invalid={errors.tableName && touched.tableName}
errorMessage={errors.tableName}
>
@ -320,12 +320,12 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
type="text"
autoComplete="off"
name="tableName"
placeholder={translate('::ListForms.ListFormEdit.DatabaseDataSourceTableName')}
placeholder={translate('::App.Listform.ListformField.TableName')}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName')}
label={translate('::App.Listform.ListformField.KeyFieldName')}
invalid={errors.keyFieldName && touched.keyFieldName}
errorMessage={errors.keyFieldName}
extra={
@ -355,7 +355,7 @@ function FormTabDatabaseDataSource(props: FormEditProps) {
isLoadingColumns
? translate('::Loading')
: translate(
'::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName',
'::App.Listform.ListformField.KeyFieldName',
)
}
options={selectCommandColumns.map((c) => ({

View file

@ -154,7 +154,7 @@ function FormTabDatabaseDelete({
<Th>{translate('::ListForms.ListFormFieldEdit.FieldName')}</Th>
<Th>{translate('::ListForms.ListFormEdit.FieldDbType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CustomValueType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.Value')}</Th>
<Th>{translate('::App.Listform.ListformField.Value')}</Th>
<Th>{translate('::ListForms.ListFormEdit.SqlQuery')}</Th>
</Tr>
</THead>

View file

@ -152,7 +152,7 @@ function FormTabDatabaseInsert({
<Th>{translate('::ListForms.ListFormFieldEdit.FieldName')}</Th>
<Th>{translate('::ListForms.ListFormEdit.FieldDbType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CustomValueType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.Value')}</Th>
<Th>{translate('::App.Listform.ListformField.Value')}</Th>
<Th>{translate('::ListForms.ListFormEdit.SqlQuery')}</Th>
</Tr>
</THead>
@ -251,7 +251,7 @@ function FormTabDatabaseInsert({
<Th>{translate('::ListForms.ListFormFieldEdit.FieldName')}</Th>
<Th>{translate('::ListForms.ListFormEdit.FieldDbType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CustomValueType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.Value')}</Th>
<Th>{translate('::App.Listform.ListformField.Value')}</Th>
<Th>{translate('::ListForms.ListFormEdit.SqlQuery')}</Th>
</Tr>
</THead>

View file

@ -107,7 +107,7 @@ function FormTabDatabaseSelect({
<Th>{translate('::ListForms.ListFormFieldEdit.FieldName')}</Th>
<Th>{translate('::ListForms.ListFormEdit.FieldDbType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CustomValueType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.Value')}</Th>
<Th>{translate('::App.Listform.ListformField.Value')}</Th>
<Th>{translate('::ListForms.ListFormEdit.SqlQuery')}</Th>
</Tr>
</THead>

View file

@ -154,7 +154,7 @@ function FormTabDatabaseUpdate({
<Th>{translate('::ListForms.ListFormFieldEdit.FieldName')}</Th>
<Th>{translate('::ListForms.ListFormEdit.FieldDbType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.CustomValueType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.Value')}</Th>
<Th>{translate('::App.Listform.ListformField.Value')}</Th>
<Th>{translate('::ListForms.ListFormEdit.SqlQuery')}</Th>
</Tr>
</THead>

View file

@ -76,7 +76,7 @@ function FormTabDetails(
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<Card className="my-2" header="General">
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsCultureName')}
label={translate('::App.Listform.ListformField.CultureName')}
invalid={errors.cultureName && touched.cultureName}
errorMessage={errors.cultureName}
>
@ -84,7 +84,7 @@ function FormTabDetails(
type="text"
autoComplete="off"
name="cultureName"
placeholder={translate('::ListForms.ListFormEdit.DetailsCultureName')}
placeholder={translate('::App.Listform.ListformField.CultureName')}
>
{({ field, form }: FieldProps<LanguageInfo>) => (
<Select

View file

@ -73,7 +73,7 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.EditingAllowEditing')}
label={translate('::ListForms.ListFormEdit.AllowEditing')}
invalid={
errors.editingOptionDto?.allowEditing &&
touched.editingOptionDto?.allowEditing
@ -82,7 +82,7 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
>
<Field
name="editingOptionDto.allowEditing"
placeholder={translate('::ListForms.ListFormEdit.EditingAllowEditing')}
placeholder={translate('::ListForms.ListFormEdit.AllowEditing')}
component={Checkbox}
/>
</FormItem>
@ -118,6 +118,20 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.EditingAllowDuplicate')}
invalid={
errors.editingOptionDto?.allowDuplicate && touched.editingOptionDto?.allowDuplicate
}
errorMessage={errors.editingOptionDto?.allowDuplicate}
>
<Field
name="editingOptionDto.allowDuplicate"
placeholder={translate('::ListForms.ListFormEdit.EditingAllowDuplicate')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.EditingAllowDeleting')}
invalid={
@ -192,7 +206,7 @@ function FormTabEdit(props: FormEditProps & { listFormCode: string }) {
</FormItem>
<FormItem
label={translate('::SidePanel.Mode')}
label={translate('::ListForms.ListFormEdit.EditingMode')}
invalid={errors.editingOptionDto?.mode && touched.editingOptionDto?.mode}
errorMessage={errors.editingOptionDto?.mode}
>

View file

@ -54,7 +54,7 @@ function FormTabEditForm(props: { listFormCode: string }) {
}}
/>
</Th>
<Th>{translate('::ListForms.ListFormEdit.EditingFormOrder')}</Th>
<Th>{translate('::App.Listform.ListformField.Order')}</Th>
<Th>{translate('::ListForms.ListFormEdit.EditingFormItemType')}</Th>
<Th>{translate('::ListForms.ListFormEdit.DetailsTitle')}</Th>
<Th>{translate('::ListForms.ListFormEdit.EditingFormColumnCount')}</Th>

View file

@ -87,7 +87,7 @@ function FormTabGantt(props: FormEditProps) {
<Card className="mt-4">
<h5 className="mb-4">{translate('::ListForms.SchedulerOptions.BasicSettings')}</h5>
<FormItem
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName')}
label={translate('::App.Listform.ListformField.KeyFieldName')}
invalid={errors.ganttOptionDto?.keyExpr && touched.ganttOptionDto?.keyExpr}
errorMessage={errors.ganttOptionDto?.keyExpr}
>
@ -95,7 +95,7 @@ function FormTabGantt(props: FormEditProps) {
type="text"
name="ganttOptionDto.keyExpr"
placeholder={translate(
'::ListForms.ListFormEdit.DatabaseDataSourceKeyFieldName',
'::App.Listform.ListformField.KeyFieldName',
)}
>
{({ field, form }: FieldProps<SelectBoxOption>) => (

Some files were not shown because too many files have changed in this diff Show more