11 KiB
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, çalışma kuralları için 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 |
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; CSSassets/css/, görsel/fontassets/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 herviews/**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.
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).
- Kurulum = kabuk.
installolayı 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. - Isıtma = bileşenler. Oturum açılınca store, yetkili
protectedrotaların view modüllerinisetWarmupRoutesile bildirir; workerENSURE_WARMUPilewarmup.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. - Güncelleme. Deploy'da herhangi bir chunk değişince entry hash'i, index.html ve
sw.jsmanifest'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. - 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'unlocalediliminde tutulur; sürüm değişince yazılan bayrağıProtectedRouteokur ve oturum açık kullanıcıyı (login şartı olmadan)/admin/changeLog'a yönlendirir, ekran bayrağı tüketir. - Runtime cache. Hash'li dosyalar cache-first (
sozsoft-assets-v1, aktivasyondawarmup.json'da olmayan eski chunk'lar silinir), DevExtreme temaları stale-while-revalidate, font/görsel cache-first./api,/connect,/swagger,/signalr,/hangfireworker'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/assetsvolume'undan servis eder;nginx/40-archive-assets.shher 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:preloadErrorfırlatır;handleChunkLoadFailureskabuk cache'ini silip sayfayı bir kez yeniler (reloadWithFreshShell; düzreloadeski 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.tsxisChunkLoadErrorile "uygulama güncellendi, yenileyin" mesajı vereloadAppbutonu 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:
node:24.22-alpine—npm run build:production -- --mode $ENV; DevExtreme lisansı yalnızca bu aşamada, tekRUNiçinde yazılıp silinir..env.$ENV'dekiVITE_API_URL/tmp/api-urlolarak ikinci aşamaya geçer.nginx:1.27-alpine—dist/+nginx/default.conf(${API_URL}envsubst ile build'de doldurulur,nginx -tile 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.