sozsoft-platform/ui/README.md
2026-10-20 13:53:21 +03:00

178 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Sözsoft Platform UI
React 19 + Vite 8 (rolldown) + DevExtreme + easy-peasy + Tailwind 4. Ekranların tamamı
veritabanından gelen ListForm/menü/route tanımlarıyla çizilir; bu klasör yalnızca çekirdek
motoru taşır. Platformun geneli için [README.md](../README.md), çalışma kuralları için
[CLAUDE.md](../CLAUDE.md).
## Komutlar
| Komut | Ne yapar |
| --- | --- |
| `npm start` | Dev sunucusu (3000). `prestart` DevExtreme lisans dosyasını üretir. |
| `npm run build` | Üretim derlemesi → `dist/`. `--mode dev|production` ile `.env.{mode}` okunur. |
| `npm run preview` | Yerel önizleme; üretim sunucusu **değildir** (bkz. Konteyner). |
| `npm run typecheck` / `npm run lint` | `tsc --noEmit` / `oxlint`. Her değişiklikten sonra ikisi de 0 hata vermeli. |
| `npm run format` | prettier + oxlint --fix. |
Ortam değişkenleri `.env` (yerel), `.env.dev`, `.env.production`: `VITE_API_URL`, `VITE_CDN_URL`.
Node/npm sürümü `package.json` → `volta`.
## Derleme çıktısı
`vite.config.ts` üç şey üretir:
- **Chunk'lar** `assets/js/entry-*.js` (giriş), `assets/js/vendor-react-*.js`,
`vendor-router-*.js` (her açılışta yüklenen, sürümler arası değişmeyen çekirdek) ve rolldown'un
otomatik böldüğü lazy chunk'lar; CSS `assets/css/`, görsel/font `assets/img|fonts/`.
Adlar hash'lidir: aynı ad = aynı içerik.
- **`sw.js`** — `src/sw.ts`'ten (`vite-plugin-pwa`, `injectManifest`). Precache manifest'i
yalnızca kabuğu (`PRECACHE_GLOBS`) taşır.
- **`warmup.json`** — `warmupManifest()` eklentisi: bütün JS/CSS chunk'larının adı/boyutu ve
her `views/**` modülünün açılması için gereken dosya zinciri (`routes`). Service worker ısıtma ve
eski sürüm temizliği için okur.
`twSafelistGenerator.ts`: custom component kodu veritabanında yaşadığı için Tailwind'in
göremediği sınıflar `safelist.txt`'ten derlemeye eklenir.
## Rota ve bileşen çözümü
`src/routes/dynamicRouteLoader.ts` `import.meta.glob('../views/**/*.tsx')` ile bütün view
modüllerini lazy olarak kaydeder; `Sas.Menus.Routes` kaydındaki `componentPath` bu haritadan
çözülür (`hasPhysicalComponent`), karşılığı olmayan rota router'a alınmaz. Runtime bileşenler
(`Custom Components`) `ComponentContext` üzerinden çizilir ve aynı yolu paylaşan fiziksel rotayı
gölgeler. Ayrıntı: [README.md §18.1](../README.md).
## PWA ve sürüm geçişi
Kod: `src/sw.ts` (service worker), `src/views/version/swRegistration.ts` (kayıt/güncelleme
motoru, entry chunk'ta, React'a bağımlı değil), `AppUpdateDialog.tsx` (lazy, kendi React
kökünde mount edilir), `installPrompt.ts` (PWA kurulum teklifi).
1. **Kurulum = kabuk.** `install` olayı yalnızca precache manifest'indeki dosyaları indirir
(index.html, entry JS/CSS, vendor chunk'ları). Biri inmezse kurulum düşer; yarım sürümle
aktive olunmaz. İndirme ilerlemesi dosya dosya istemciye yayınlanır.
2. **Isıtma = bileşenler.** Oturum açılınca store, yetkili `protected` rotaların view
modüllerini `setWarmupRoutes` ile bildirir; worker `ENSURE_WARMUP` ile `warmup.json`'daki
zinciri arka planda indirir. Ziyaretçi ve giriş ekranı hiçbir bileşen indirmez. Best-effort:
inemeyen dosya atlanır, süre bütçesi dolunca kalanlar runtime cache'e kalır.
3. **Güncelleme.** Deploy'da herhangi bir chunk değişince entry hash'i, index.html ve `sw.js`
manifest'i değişir; `registration.update()` (kayıt, sekmeye dönüş, bağlantı, 30 dk yoklama)
yeni worker'ı görür → kurulum → `SKIP_WAITING` → `controllerchange` → yenileme. Pencere geçiş
bitene kadar kapanmaz; ilk kurulumda (controller yok) kapatılabilir.
4. **Sürüm numarası ve notlar backend'dedir** (`extraProperties.changeLogs`,
`/api/app/change-log`); UI'da sürüm dosyası yoktur. Son görülen sürüm store'un `locale`
diliminde tutulur; sürüm değişince yazılan bayrağı `ProtectedRoute` okur ve oturum açık
kullanıcıyı (login şartı olmadan) `/admin/changeLog`'a yönlendirir, ekran bayrağı tüketir.
5. **Runtime cache.** Hash'li dosyalar cache-first (`sozsoft-assets-v1`, aktivasyonda
`warmup.json`'da olmayan eski chunk'lar silinir), DevExtreme temaları stale-while-revalidate,
font/görsel cache-first. `/api`, `/connect`, `/swagger`, `/signalr`, `/hangfire` worker'a
girmez. Gezinmeler kabuk cache'indeki index.html ile anında açılır, kopya arka planda tazelenir.
### Deploy sonrası eksik / değişmiş chunk
İki katman birlikte çalışır:
- **Sunucu tarafı (asıl çözüm):** konteyner nginx'i hash'li `assets/` dosyalarını `/srv/assets`
volume'undan servis eder; `nginx/40-archive-assets.sh` her açılışta yeni sürümün dosyalarını
arşive ekler (var olanı ezmez), imajda artık olmayan ve 30 günden eski dosyaları siler. Açık
sekmeler deploy'dan sonra da kendi sürümlerinin chunk'larını bulur.
- **İstemci tarafı (yedek):** bir chunk yine de bulunamazsa Vite `vite:preloadError`
fırlatır; `handleChunkLoadFailures` kabuk cache'ini silip sayfayı **bir kez** yeniler
(`reloadWithFreshShell`; düz `reload` eski index.html'i geri getirirdi). Koruma bayrağı
(`localStorage.chunkReloadGuard`, 10 s) döngüyü keser; ikinci başarısızlık hata sınırına
düşer, `Views.tsx` `isChunkLoadError` ile "uygulama güncellendi, yenileyin" mesajı ve
`reloadApp` butonu gösterir. Hata sınırı rota değişince kendini sıfırlar (`resetKeys`).
Dev'de service worker kapalıdır (`VITE_PWA_DEV=true` ile açılır); kapalıyken önceki kayıtlar
temizlenir, yoksa `sw.js` yerine index.html döner ve "unsupported MIME type" hatası üretirdi.
## Konteyner
`Sozsoft.Platform.Ui.Dockerfile` iki aşamalıdır:
1. `node:24.22-alpine` — `npm run build:production -- --mode $ENV`; DevExtreme lisansı yalnızca
bu aşamada, tek `RUN` içinde yazılıp silinir. `.env.$ENV`'deki `VITE_API_URL` `/tmp/api-url`
olarak ikinci aşamaya geçer.
2. `nginx:1.27-alpine` — `dist/` + `nginx/default.conf` (`${API_URL}` envsubst ile build'de
doldurulur, `nginx -t` ile doğrulanır) + `/docker-entrypoint.d/40-archive-assets.sh`.
`nginx/default.conf` sözleşmesi:
| Yol | Cache-Control | Not |
| --- | --- | --- |
| `/assets/**` | `public, max-age=31536000, immutable` | `/srv/assets` (imaj + arşiv) |
| `/index.html`, `/sw.js`, `/warmup.json`, `/manifest.webmanifest` | `no-store, no-cache, must-revalidate` | sürüm kapısı |
| `/css/**` (DevExtreme temaları) | `public, max-age=86400` | hash'siz, SW tazeler |
| `/img/**` | `public, max-age=604800` | |
| `/sitemap.xml`, `/robots.txt` | — | API'ye proxy (`SeoController`) |
| diğer | — | SPA fallback → `index.html` |
Compose (`configs/deployment/docker-compose-app.yml`): `ui` servisi `3002:80`, volume
`ui-assets:/srv/assets`. Deploy (`scripts/deploy/app.sh`) konteynerleri önceden durdurmaz:
`pull` → `up -d`; kesinti yalnızca konteyner değişimi kadardır. Ters vekil `nginx.conf`
Cache-Control başlıklarını ezmez; başlıklar konteynerden gelir, tenant alt alan adları da aynı
başlıkları alır.
## Kod düzeni
```
src/
components/ paylaşılan bileşenler (ui/, shared/, template/, layouts/, list/form editörleri…)
views/ rota bileşenleri (import.meta.glob ile lazy); views/shared/ hariç
routes/ dinamik router ve route çözümü
store/ easy-peasy modelleri (abpConfig, auth, locale, base…)
services/ axios istemcileri (authApi, platformApi) ve SignalR servisleri
proxy/ API DTO tipleri
utils/ yardımcılar ve hook'lar
sw.ts service worker
```
Grid/Tree popup'ı ile form sayfası editör yeteneklerini `views/list/shared/*` ve
`form/editors/*` üzerinden paylaşır; `scriptRecipes.ts` C# `Domain.Shared/Editors/*` ile
birebir aynı çıktıyı üretmek zorundadır (bkz. CLAUDE.md "Cross-cutting"). Grid ve Tree'nin ortak
iç seçenekleri (düzenleme, araç çubuğu, süzgeçler, hücre editörü şablonları) tek yerde çizilir:
`views/list/shared/listOptionChildren.tsx`.
Ortak yardımcılar; aynı işin ikinci bir kopyası yazılmaz:
| İş | Yardımcı |
| --- | --- |
| Kullanıcıya görünen metin | `useLocalization().translate('::Anahtar')`; React dışı kodda `utils/translate.ts` `translateStatic` |
| Toast bildirimi | `utils/notify.tsx` `notifySuccess/Info/Warning/Danger(başlık?, { message })` |
| API hatasının bildirimi | `services/apiError.ts` `pushApiError` |
| Kimlik üretimi | `utils/id.ts` `createId` (UUID), `createShortId` |
| Tarih/sayı/para/boyut | `utils/localeFormat.ts` (aşağıda) |
## Tarih ve sayı biçimi
Kullanıcıya görünen her tarih ve sayı **aktif kültürle** biçimlenir; kültür `useLocale` ile store'dan
`<html lang>`'e yazılır ve tek giriş `src/utils/localeFormat.ts`'tir:
| İhtiyaç | Çağrı |
| --- | --- |
| Tarih / saat / tarih+saat | `formatLocaleDate`, `formatLocaleTime`, `formatLocaleDateTime` (dayjs `L`, `LT`, `L LT`); uzun tarih `formatLocaleLongDate` (`LL`) |
| Sayı | `formatLocaleNumber(v, { minimumFractionDigits, maximumFractionDigits, useGrouping })` — varsayılan binlik ayracı **yok** (yıl, kimlik, adet), tutar için `useGrouping: true` |
| Sabit ondalık / dosya boyutu | `formatLocaleFixed(v, 2)`, `formatLocaleBytes(bytes)` |
| Para | `formatLocaleCurrency(v, currencyCode)` — kod veriden gelir; verisi olmayan fiyat `DEFAULT_CURRENCY` (`constants/app.constant.ts`) |
| Ham endpoint değeri | `formatLocaleValue(v)` (ISO tarih ve sayıyı çevirir, diğerini olduğu gibi bırakır) |
Hepsi boş/geçersiz değer için `''` döner ve `Date | string | number | Dayjs` alır. Tarih seçiciler
(`DatePicker`, `DateTimepicker`) `inputFormat` verilmeyince kültürün desenini kullanır; prop yalnızca
makine biçimi gerektiğinde verilir.
DevExtreme kolon/editör biçimleri **adlı** biçimdir: `shortDate`, `shortDateShortTime`, `shortTime`,
`decimal`, `fixedPoint`, `currency` (`EditorOptions.Date/DateTime/Time` ↔ `presets.ts`). Kültür
`DevExtremeBoundary` → `configureDevExtreme` ile verilir: `locale(cultureName)` tarih/sayıyı,
`loadMessages` ile yüklenen `devextreme/localization/messages/{dil}.json` arayüz metinlerini (Evet/Hayır,
"Veri yok", filtre satırı) çevirir; paket dışı dil İngilizce metinle kalır.
Yazılmayanlar: `'DD.MM.YYYY'`, `'HH:mm'` gibi sabit dayjs desenleri, `toLocaleString()` /
`toLocaleDateString('tr-TR')`, `Intl.*Format('tr-TR')`, görüntülenen ondalık için `toFixed`. Sunucu tarafı
karşılığı `CultureFormat` + `CultureInfo.CurrentCulture`'dır.
**Makine biçimi serbesttir** ve `localeFormat`'tan geçmez: `<input type="date|datetime-local">` değeri ve
API yüküne giden ISO metin (`YYYY-MM-DD`, `YYYY-MM-DDTHH:mm:ss`), gruplama/sorgu anahtarı, DevExtreme
`dateSerializationFormat`, yakınlaştırma yüzdesi gibi sayısal hesap yuvarlaması (`Number(x.toFixed(2))`),
metin karşılaştırması için `toLocaleLowerCase('tr-TR')`. `AppUpdateDialog` uygulama ağacının dışında,
kültür bilinmeden açıldığı için kendi `formatBytes` kopyasını taşır.