Compare commits
27 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ac72ad19b | ||
|
|
c26f0cb5bc | ||
|
|
3db9fc332b | ||
|
|
e9ce256c07 | ||
|
|
9fb838dcba | ||
|
|
b0dd72ec57 | ||
|
|
9d5c5ccf09 | ||
|
|
30be61f2c7 | ||
|
|
08c495943b | ||
|
|
6177dbcf24 | ||
|
|
87ba256bae | ||
|
|
9e64976963 | ||
|
|
130d35c377 | ||
|
|
8c54c6590c | ||
|
|
27ff19ca0d | ||
|
|
f57fbda2d6 | ||
|
|
3baa7def61 | ||
|
|
cf6ded1105 | ||
|
|
a7e8d7995b | ||
|
|
62f38a27a5 | ||
|
|
17df35102d | ||
|
|
203160fce0 | ||
|
|
4943f78f89 | ||
|
|
dd82d405ce | ||
|
|
cb4a74bf81 | ||
|
|
401db7bfef | ||
|
|
95b8b8e798 |
149 changed files with 4673 additions and 3125 deletions
711
.github/instructions/ai.instructions.md
vendored
Normal file
711
.github/instructions/ai.instructions.md
vendored
Normal 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.
|
||||
149
.github/instructions/list.instructions.md
vendored
Normal file
149
.github/instructions/list.instructions.md
vendored
Normal 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.
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public interface IAuditLogAppService
|
|||
|
||||
}
|
||||
|
||||
[Authorize(AppCodes.AuditLogs)]
|
||||
[Authorize(AppCodes.IdentityManagement.AuditLogs)]
|
||||
public class AuditLogAppService
|
||||
: CrudAppService<AuditLog, AuditLogDto, Guid>
|
||||
, IAuditLogAppService
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
]}
|
||||
|
|
@ -1080,7 +1080,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.Int32,
|
||||
FieldName = "Id",
|
||||
CaptionName = "App.Listform.ListformField.Id",
|
||||
CaptionName = "App.Listform.ListformField.Id",
|
||||
Width = 100,
|
||||
ListOrderNo = 1,
|
||||
Visible = false,
|
||||
|
|
@ -1095,7 +1095,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "System",
|
||||
CaptionName = "App.Listform.ListformField.System",
|
||||
CaptionName = "App.Listform.ListformField.System",
|
||||
Width = 100,
|
||||
ListOrderNo = 2,
|
||||
Visible = true,
|
||||
|
|
@ -1111,7 +1111,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Group",
|
||||
CaptionName = "App.Listform.ListformField.Group",
|
||||
CaptionName = "App.Listform.ListformField.Group",
|
||||
Width = 400,
|
||||
ListOrderNo = 3,
|
||||
Visible = true,
|
||||
|
|
@ -1127,15 +1127,21 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
|||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
FieldName = "Term",
|
||||
CaptionName = "App.Listform.ListformField.Term",
|
||||
CaptionName = "App.Listform.ListformField.Term",
|
||||
Width = 400,
|
||||
ListOrderNo = 4,
|
||||
Visible = true,
|
||||
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),
|
||||
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||
PivotSettingsJson = DefaultPivotSettingsJson
|
||||
},
|
||||
new() {
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,7 @@ public enum OperationEnum
|
|||
Delete,
|
||||
DeleteBefore,
|
||||
DeleteAfter,
|
||||
Select
|
||||
Select,
|
||||
Duplicate,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
|
|
@ -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),
|
||||
|
|
@ -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)");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@
|
|||
"StringEncryption": {
|
||||
"DefaultPassPhrase": "UQpiYfT79zRZ3yYH"
|
||||
},
|
||||
"Setup": {
|
||||
"MigratorPath": "/srv/Sozsoft.Platform.DbMigrator",
|
||||
"MigratorArgs": "--Seed=true"
|
||||
},
|
||||
"Serilog": {
|
||||
"MinimumLevel": {
|
||||
"Default": "Information"
|
||||
|
|
|
|||
|
|
@ -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
71
claude.md
Normal 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
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
}
|
||||
|
||||
.drawer-body {
|
||||
@apply p-6 h-full overflow-y-auto;
|
||||
@apply p-4 h-full overflow-y-auto;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
|
|
|
|||
|
|
@ -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 || ''}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,42 @@ 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
|
||||
getConfig(false)
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
124
ui/src/components/template/ThemeConfigurator/StyleSwitcher.tsx
Normal file
124
ui/src/components/template/ThemeConfigurator/StyleSwitcher.tsx
Normal 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
|
||||
|
|
@ -3,49 +3,60 @@ 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
|
||||
callBackClose?: () => void
|
||||
}
|
||||
|
||||
const ThemeConfigurator = ({ callBackClose }: ThemeConfiguratorProps) => {
|
||||
const { translate } = useLocalization()
|
||||
|
||||
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>
|
||||
<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} />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">{translate('::SidePanel.NavMode')}</h6>
|
||||
<NavModeSwitcher />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">{translate('::SidePanel.Themed')}</h6>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">{translate('::SidePanel.Layout')}</h6>
|
||||
<LayoutSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
<CopyButton />
|
||||
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-3 mb-2">
|
||||
<div>
|
||||
<h6 className="mb-3">{translate('::App.SiteManagement.Theme.Style')}</h6>
|
||||
<StyleSwitcher onStyleChange={handleStyleChange} />
|
||||
</div>
|
||||
)
|
||||
<div>
|
||||
<h6 className="mb-3">{translate('::SidePanel.NavMode')}</h6>
|
||||
<NavModeSwitcher />
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeConfigurator
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export const PREFIX = "App";
|
||||
|
||||
export const DIR_RTL = 'rtl'
|
||||
export const DIR_LTR = 'ltr'
|
||||
export const MODE_LIGHT = 'light'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ export interface AuditLogDto {
|
|||
applicationName: string
|
||||
userId?: string
|
||||
userName?: string
|
||||
tenantId?: string
|
||||
tenantName?: string
|
||||
executionTime: string
|
||||
executionDuration: number
|
||||
clientIpAddress?: string
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -33,4 +33,5 @@ export type ThemeConfig = {
|
|||
type: LayoutType
|
||||
sideNavCollapse: boolean
|
||||
}
|
||||
style: string
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export const ROUTES_ENUM = {
|
||||
setup: '/setup',
|
||||
public: {
|
||||
home: '/home',
|
||||
about: '/about',
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
21
ui/src/services/setup.service.ts
Normal file
21
ui/src/services/setup.service.ts
Normal 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`
|
||||
}
|
||||
|
|
@ -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
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
state.navMode = payload
|
||||
}),
|
||||
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
|
||||
}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() || ''
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(true)
|
||||
}, [page])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [filter])
|
||||
const handleFilterChange = (value: string[]) => {
|
||||
setPage(0)
|
||||
setNotifications({})
|
||||
setHasMore(false)
|
||||
setFilter(value)
|
||||
setIsFilterDrawerOpen(false)
|
||||
}
|
||||
|
||||
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>
|
||||
<Log
|
||||
notifications={notifications}
|
||||
isLoading={loading}
|
||||
onLoadMore={() => setPage(page + 1)}
|
||||
loadable={hasMore}
|
||||
></Log>
|
||||
<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((prev) => prev + 1)}
|
||||
loadable={hasMore}
|
||||
></Log>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<LogFilter filter={filter} onFilterChange={handleFilterChange} useAffix />
|
||||
</div>
|
||||
</div>
|
||||
<LogFilter filter={filter} onFilterChange={(value: string[]) => setFilter(value)} />
|
||||
</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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,35 +33,43 @@ 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>
|
||||
<Checkbox.Group
|
||||
vertical
|
||||
value={filter}
|
||||
onChange={(value) => {
|
||||
onFilterChange(value as string[])
|
||||
}}
|
||||
>
|
||||
<CategoryTitle className="mb-3">
|
||||
{translate('::Abp.Identity.ActivityLogs.Channels')}
|
||||
</CategoryTitle>
|
||||
{ticketCheckboxes.map((checkbox) => (
|
||||
<Checkbox key={checkbox.value} className="mb-4" value={checkbox.value}>
|
||||
{checkbox.label}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</Affix>
|
||||
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}
|
||||
onChange={(value) => {
|
||||
onFilterChange(value as string[])
|
||||
}}
|
||||
>
|
||||
<CategoryTitle className="mb-3 text-gray-500">
|
||||
{translate('::Abp.Identity.ActivityLogs.Channels')}
|
||||
</CategoryTitle>
|
||||
{ticketCheckboxes.map((checkbox) => (
|
||||
<Checkbox key={checkbox.value} className="mb-3" value={checkbox.value}>
|
||||
{checkbox.label}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (useAffix) {
|
||||
return <Affix offset={80}>{content}</Affix>
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export default LogFilter
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
fetchTenants()
|
||||
}, [fetchTenants])
|
||||
if (isHostContext) {
|
||||
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,29 +745,38 @@ const FileManager = () => {
|
|||
{/* Tenant Selector Row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<FaBuilding className="text-gray-500 flex-shrink-0" />
|
||||
<Select
|
||||
size="xs"
|
||||
isLoading={tenantsLoading}
|
||||
options={[
|
||||
{ value: '', label: 'Host' },
|
||||
...tenants.map((t) => ({ value: t.id ?? '', label: t.name ?? '' })),
|
||||
]}
|
||||
value={{
|
||||
value: selectedTenant ? selectedTenant.id : '',
|
||||
label: selectedTenant ? selectedTenant.name : 'Host',
|
||||
}}
|
||||
onChange={(option) => {
|
||||
if (option && 'value' in option) {
|
||||
const val = option.value as string
|
||||
if (!val) {
|
||||
setSelectedTenant(undefined)
|
||||
} else {
|
||||
const found = tenants.find((t) => t.id === val)
|
||||
if (found) setSelectedTenant({ id: found.id!, name: found.name ?? '' })
|
||||
{isHostContext ? (
|
||||
<Select
|
||||
size="xs"
|
||||
isLoading={tenantsLoading}
|
||||
options={[
|
||||
{ value: '', label: 'Host' },
|
||||
...tenants.map((t) => ({ value: t.id ?? '', label: t.name ?? '' })),
|
||||
]}
|
||||
value={{
|
||||
value: selectedTenant ? selectedTenant.id : '',
|
||||
label: selectedTenant ? selectedTenant.name : 'Host',
|
||||
}}
|
||||
onChange={(option) => {
|
||||
if (option && 'value' in option) {
|
||||
const val = option.value as string
|
||||
if (!val) {
|
||||
setSelectedTenant(undefined)
|
||||
} else {
|
||||
const found = tenants.find((t) => t.id === val)
|
||||
if (found) setSelectedTenant({ id: found.id!, name: found.name ?? '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue