Update new version checking and Report Categories
This commit is contained in:
parent
1d05d24e41
commit
28ec641275
13 changed files with 413 additions and 80 deletions
|
|
@ -19949,6 +19949,72 @@
|
|||
"key": "App.Tenant.Create",
|
||||
"en": "Tenant Create",
|
||||
"tr": "Tenant Oluştur"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.CheckUpdate",
|
||||
"en": "Checking...",
|
||||
"tr": "Kontrol ediliyor..."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.CheckForUpdates",
|
||||
"en": "Check for Updates",
|
||||
"tr": "Güncellemeyi Kontrol Et"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NotSupportServiceWorkerUpdate",
|
||||
"en": "Your browser does not support service worker updates.",
|
||||
"tr": "Tarayıcınız service worker güncellemelerini desteklemiyor."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NewVersionAvailable",
|
||||
"en": "A new service worker has been found. The application will automatically update when the files are ready.",
|
||||
"tr": "Dosyalar hazırlandığında uygulama otomatik olarak güncellenecek."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ManualUpdateFailed",
|
||||
"en": "Please check your connection and try again.",
|
||||
"tr": "Lütfen bağlantınızı kontrol edip tekrar deneyin."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ChangeLogDescription",
|
||||
"en": "Check if there's a new application version available on the server.",
|
||||
"tr": "Sunucuda yeni bir uygulama sürümü olup olmadığını kontrol edin."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NoNewVersionFound",
|
||||
"en": "No new version was found available.",
|
||||
"tr": "Kullanılabilir yeni bir sürüm bulunamadı."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpToDate",
|
||||
"en": "The app is up to date.",
|
||||
"tr": "Uygulama güncel"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateCouldNotBeChecked",
|
||||
"en": "Update could not be checked",
|
||||
"tr": "Güncelleme kontrol edilemedi"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.NewVersionFound",
|
||||
"en": "New version found.",
|
||||
"tr": "Yeni sürüm bulundu"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.UpdateCheckFailed",
|
||||
"en": "Update check failed.",
|
||||
"tr": "Güncelleme kontrolü başarısız"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1417,7 +1417,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportCategory)),
|
||||
KeyFieldName = "Id",
|
||||
DefaultFilter = DefaultFilterJson,
|
||||
KeyFieldDbSourceType = DbType.Int32,
|
||||
KeyFieldDbSourceType = DbType.Guid,
|
||||
SortMode = GridOptions.SortModeSingle,
|
||||
FilterRowJson = DefaultFilterRowJson(),
|
||||
HeaderFilterJson = DefaultHeaderFilterJson(),
|
||||
|
|
@ -1439,8 +1439,8 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
]}
|
||||
}),
|
||||
DeleteCommand = $"UPDATE \"{FullNameTable(TableNameEnum.ReportCategory)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
|
||||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(DbType.String),
|
||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
|
||||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
|
||||
});
|
||||
|
||||
#region Report Categories Fields
|
||||
|
|
@ -1582,7 +1582,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
AuthName = listFormName,
|
||||
Url=$"/admin/reports/DynamicReport/view/@Id",
|
||||
IsVisible = true,
|
||||
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Raporlar')"
|
||||
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Raporlar')"
|
||||
},
|
||||
new() {
|
||||
Hint = "App.Platform.Design",
|
||||
|
|
@ -1591,7 +1591,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
AuthName = listFormName + ".Update",
|
||||
Url=$"/admin/reports/DynamicReport/design/@Id",
|
||||
IsVisible = true,
|
||||
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Raporlar')"
|
||||
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Raporlar')"
|
||||
},
|
||||
new() {
|
||||
Hint = "App.Platform.View",
|
||||
|
|
@ -1600,7 +1600,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
AuthName = listFormName,
|
||||
Url=$"/admin/reports/DynamicGrid/view/@Name",
|
||||
IsVisible = true,
|
||||
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Listeler')"
|
||||
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Listeler')"
|
||||
},
|
||||
new() {
|
||||
Hint = "App.Platform.View",
|
||||
|
|
@ -1609,7 +1609,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
AuthName = listFormName,
|
||||
Url=$"/admin/reports/DynamicGrid/view/@Id/@Name",
|
||||
IsVisible = true,
|
||||
VisibleExpression = "((e)=> e.row.data.CategoryId === 'Formlar')"
|
||||
VisibleExpression = "((e)=> e.component.columnOption('CategoryId').lookup.calculateCellValue(e.row.data.CategoryId) === 'Formlar')"
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
|
@ -1660,7 +1660,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
|
|||
new() {
|
||||
ListFormCode = listForm.ListFormCode,
|
||||
CultureName = LanguageCodes.En,
|
||||
SourceDbType = DbType.String,
|
||||
SourceDbType = DbType.Guid,
|
||||
FieldName = "CategoryId",
|
||||
CaptionName = "App.Listform.ListformField.CategoryId",
|
||||
Width = 0,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using Volo.Abp.MultiTenancy;
|
|||
|
||||
namespace Sozsoft.Platform.Entities
|
||||
{
|
||||
public class ReportCategory : FullAuditedEntity<string>, IMultiTenant
|
||||
public class ReportCategory : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Guid? TenantId { get; set; }
|
||||
|
||||
|
|
@ -15,9 +15,12 @@ namespace Sozsoft.Platform.Entities
|
|||
|
||||
public ICollection<ReportTemplate> ReportTemplates { get; set; }
|
||||
|
||||
public ReportCategory(string id)
|
||||
protected ReportCategory()
|
||||
{
|
||||
}
|
||||
|
||||
public ReportCategory(Guid id) : base(id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Sozsoft.Platform.Entities
|
|||
public string Description { get; set; }
|
||||
public string HtmlContent { get; set; }
|
||||
|
||||
public string CategoryId { get; set; }
|
||||
public Guid CategoryId { get; set; }
|
||||
public ReportCategory ReportCategory { get; set; }
|
||||
|
||||
public string Status { get; set; }
|
||||
|
|
@ -23,7 +23,7 @@ namespace Sozsoft.Platform.Entities
|
|||
string name,
|
||||
string description,
|
||||
string htmlContent,
|
||||
string categoryId,
|
||||
Guid categoryId,
|
||||
string status
|
||||
) : base(id)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -660,7 +660,6 @@ public class PlatformDbContext :
|
|||
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ReportCategory)), Prefix.DbSchema);
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Id).HasMaxLength(256);
|
||||
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.Description).HasMaxLength(4096); // JSON string
|
||||
b.Property(x => x.Icon).HasMaxLength(64);
|
||||
|
|
@ -674,7 +673,6 @@ public class PlatformDbContext :
|
|||
b.ConfigureByConvention();
|
||||
|
||||
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
|
||||
b.Property(x => x.CategoryId).HasMaxLength(256);
|
||||
b.Property(x => x.Description).HasMaxLength(1024);
|
||||
b.Property(x => x.HtmlContent).IsRequired();
|
||||
b.Property(x => x.Status).HasMaxLength(20);
|
||||
|
|
|
|||
|
|
@ -4482,9 +4482,8 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
|
|
@ -4547,9 +4546,8 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CategoryId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
|
|
|
|||
|
|
@ -650,7 +650,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
name: "Adm_T_ReportCategory",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(max)", maxLength: 4096, nullable: true),
|
||||
|
|
@ -2259,7 +2259,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
HtmlContent = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
CategoryId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
||||
CategoryId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
|
|
|
|||
|
|
@ -4479,9 +4479,8 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
modelBuilder.Entity("Sozsoft.Platform.Entities.ReportCategory", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
|
|
@ -4544,9 +4543,8 @@ namespace Sozsoft.Platform.Migrations
|
|||
b.Property<Guid>("Id")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CategoryId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
b.Property<Guid>("CategoryId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreationTime")
|
||||
.HasColumnType("datetime2")
|
||||
|
|
|
|||
|
|
@ -491,7 +491,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
private readonly IRepository<GlobalSearch, int> _globalSearch;
|
||||
private readonly IRepository<CustomEndpoint, Guid> _customEndpointRepository;
|
||||
private readonly IRepository<CustomComponent, Guid> _customComponentRepository;
|
||||
private readonly IRepository<ReportCategory, string> _reportCategoriesRepository;
|
||||
private readonly IRepository<ReportCategory, Guid> _reportCategoriesRepository;
|
||||
private readonly IRepository<ReportTemplate, Guid> _reportTemplatesRepository;
|
||||
private readonly IRepository<Home, Guid> _homeRepository;
|
||||
private readonly IRepository<About, Guid> _aboutRepository;
|
||||
|
|
@ -548,7 +548,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
IRepository<SkillLevel, string> skillLevelRepository,
|
||||
IRepository<CustomEndpoint, Guid> customEndpointRepository,
|
||||
IRepository<CustomComponent, Guid> customComponentRepository,
|
||||
IRepository<ReportCategory, string> reportCategoriesRepository,
|
||||
IRepository<ReportCategory, Guid> reportCategoriesRepository,
|
||||
IRepository<ReportTemplate, Guid> reportTemplatesRepository,
|
||||
IRepository<Home, Guid> homeRepository,
|
||||
IRepository<About, Guid> aboutRepository,
|
||||
|
|
@ -867,7 +867,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
|
||||
if (!exists)
|
||||
{
|
||||
await _reportCategoriesRepository.InsertAsync(new ReportCategory(item.Name)
|
||||
await _reportCategoriesRepository.InsertAsync(new ReportCategory(Guid.NewGuid())
|
||||
{
|
||||
Name = item.Name,
|
||||
Description = item.Description,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
{
|
||||
"commit": "e7be51a",
|
||||
"commit": "1d05d24",
|
||||
"releases": [
|
||||
{
|
||||
"version": "1.1.07",
|
||||
"buildDate": "2026-07-13",
|
||||
"commit": "50cb3eaf979eb5027172a92a4e941ed64aa3671e",
|
||||
"changeLog": [
|
||||
"- localStorage tüm bilgiler easy-peasy içerisinde birleştirildi.",
|
||||
"- Codex 5.6 sol optimizasyon yapıldı. Bir çok komponent Lazy load ile yüklendi.",
|
||||
"- Workflow komponentinden birden fazla kişi onay verebilir.",
|
||||
"- Posta Kodu eklendi.",
|
||||
"- Sales Order kısmında Yeni ve Eski müşteri sipariş ayrımı yapıldı"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.1.06",
|
||||
"buildDate": "2026-07-06",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useEffect, useState } from "react"
|
||||
import AdaptableCard from "@/components/shared/AdaptableCard"
|
||||
import Container from "@/components/shared/Container"
|
||||
import { FaTag, FaRegCheckCircle } from "react-icons/fa"
|
||||
import { useLocalization } from "@/utils/hooks/useLocalization"
|
||||
import { Helmet } from "react-helmet"
|
||||
import { APP_NAME } from "@/constants/app.constant"
|
||||
import AdaptableCard from '@/components/shared/AdaptableCard'
|
||||
import Container from '@/components/shared/Container'
|
||||
import { Button, Notification, toast } from '@/components/ui'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { FaRegCheckCircle, FaSyncAlt, FaTag } from 'react-icons/fa'
|
||||
import { checkForAppUpdate } from './swRegistration'
|
||||
|
||||
type Release = {
|
||||
version: string
|
||||
|
|
@ -12,7 +14,15 @@ type Release = {
|
|||
changeLog: string[]
|
||||
}
|
||||
|
||||
const Log = ({ version, date, children }: { version: string; date: string; children?: React.ReactNode }) => {
|
||||
const Log = ({
|
||||
version,
|
||||
date,
|
||||
children,
|
||||
}: {
|
||||
version: string
|
||||
date: string
|
||||
children?: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative pl-4 sm:pl-32 py-4 group">
|
||||
<div className="flex flex-col sm:flex-row items-start mb-1 group-last:before:hidden before:absolute before:left-2 sm:before:left-0 before:h-full before:px-px before:bg-slate-200 sm:before:ml-[6.5rem] before:self-start before:-translate-x-1/2 before:translate-y-3 after:absolute after:left-2 sm:after:left-0 after:w-2 after:h-2 after:bg-indigo-600 after:border-4 after:box-content after:border-slate-50 after:rounded-full sm:after:ml-[6.5rem] after:-translate-x-1/2 after:translate-y-1.5">
|
||||
|
|
@ -20,8 +30,7 @@ const Log = ({ version, date, children }: { version: string; date: string; child
|
|||
{date}
|
||||
</time>
|
||||
<div className="flex items-center text-xl font-bold text-gray-900 dark:text-white">
|
||||
<FaTag className="mr-2 text-indigo-500" />
|
||||
v{version}
|
||||
<FaTag className="mr-2 text-indigo-500" />v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-slate-500">{children}</div>
|
||||
|
|
@ -32,9 +41,10 @@ const Log = ({ version, date, children }: { version: string; date: string; child
|
|||
const Changelog = () => {
|
||||
const { translate } = useLocalization()
|
||||
const [releases, setReleases] = useState<Release[]>([])
|
||||
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/version.json?ts=" + Date.now())
|
||||
fetch('/version.json?ts=' + Date.now())
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data?.releases) {
|
||||
|
|
@ -44,15 +54,73 @@ const Changelog = () => {
|
|||
.catch(() => setReleases([]))
|
||||
}, [])
|
||||
|
||||
const handleManualUpdate = async () => {
|
||||
if (isCheckingUpdate) return
|
||||
setIsCheckingUpdate(true)
|
||||
|
||||
try {
|
||||
const result = await checkForAppUpdate()
|
||||
|
||||
if (result === 'up-to-date') {
|
||||
toast.push(
|
||||
<Notification title={translate('::App.UpToDate')} type="success">
|
||||
{translate('::App.NoNewVersionFound')}
|
||||
</Notification>,
|
||||
{ placement: 'bottom-end' },
|
||||
)
|
||||
} else if (result === 'unsupported') {
|
||||
toast.push(
|
||||
<Notification title={translate('::App.UpdateCouldNotBeChecked')} type="warning">
|
||||
{translate('::App.NotSupportServiceWorkerUpdate')}
|
||||
</Notification>,
|
||||
{ placement: 'bottom-end' },
|
||||
)
|
||||
} else {
|
||||
toast.push(
|
||||
<Notification title={translate('::App.NewVersionFound')} type="info">
|
||||
{translate('::App.NewVersionAvailable')}
|
||||
</Notification>,
|
||||
{ placement: 'bottom-end' },
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Manual application update failed.', error)
|
||||
toast.push(
|
||||
<Notification title={translate('::App.UpdateCheckFailed')} type="danger">
|
||||
{translate('::App.ManualUpdateFailed')}
|
||||
</Notification>,
|
||||
{ placement: 'bottom-end' },
|
||||
)
|
||||
} finally {
|
||||
setIsCheckingUpdate(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Helmet
|
||||
titleTemplate={`%s | ${APP_NAME}`}
|
||||
title={translate("::App.ChangeLog")}
|
||||
title={translate('::App.ChangeLog')}
|
||||
defaultTitle={APP_NAME}
|
||||
/>
|
||||
<AdaptableCard>
|
||||
<div className="p-2">
|
||||
<div className="mb-6 flex flex-col gap-3 border-b border-gray-200 pb-5 dark:border-gray-700 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="mb-1">{translate('::App.ChangeLog')}</h3>
|
||||
<p className="text-sm text-gray-500">{translate('::App.ChangeLogDescription')}</p>
|
||||
</div>
|
||||
<Button
|
||||
icon={<FaSyncAlt />}
|
||||
loading={isCheckingUpdate}
|
||||
variant="solid"
|
||||
onClick={handleManualUpdate}
|
||||
>
|
||||
{isCheckingUpdate
|
||||
? translate('::App.CheckUpdate')
|
||||
: translate('::App.CheckForUpdates')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{releases.map((rel) => (
|
||||
<Log key={rel.version} version={rel.version} date={rel.buildDate}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
import { registerSW } from 'virtual:pwa-register'
|
||||
|
||||
const ACTIVATION_RETRY_DELAY = 5_000
|
||||
const ACTIVATION_TIMEOUT = 30_000
|
||||
|
||||
let registrationStarted = false
|
||||
let activationInProgress = false
|
||||
let reloadStarted = false
|
||||
let currentRegistration: ServiceWorkerRegistration | undefined
|
||||
let activateUpdateHandler: ((reloadPage?: boolean) => Promise<void>) | undefined
|
||||
let registrationInitialization: Promise<void> | undefined
|
||||
let activationRetryTimer: number | undefined
|
||||
let activationTimeoutTimer: number | undefined
|
||||
const watchedInstallingWorkers = new WeakSet<ServiceWorker>()
|
||||
|
||||
export type ManualUpdateResult = 'up-to-date' | 'updating' | 'unsupported'
|
||||
|
||||
function showUpdateOverlay() {
|
||||
if (document.getElementById('sw-update-overlay')) return
|
||||
|
||||
|
|
@ -67,30 +82,202 @@ function showUpdateOverlay() {
|
|||
document.body.appendChild(overlay)
|
||||
}
|
||||
|
||||
export const registerServiceWorker = () => {
|
||||
// autoUpdate modunda onNeedRefresh tetiklenmez.
|
||||
// updatefound → yeni SW installing → overlay aç.
|
||||
// skipWaiting: true (workbox config) → controllerchange → autoUpdate reload.
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
// ready'den önce updatefound zaten tetiklendiyse (race condition)
|
||||
if (registration.installing && navigator.serviceWorker.controller) {
|
||||
showUpdateOverlay()
|
||||
}
|
||||
function hideUpdateOverlay() {
|
||||
document.getElementById('sw-update-overlay')?.remove()
|
||||
document.getElementById('sw-update-overlay-style')?.remove()
|
||||
}
|
||||
|
||||
registration.addEventListener('updatefound', () => {
|
||||
// Güncelleme başlar başlamaz overlay'i göster
|
||||
if (navigator.serviceWorker.controller) {
|
||||
showUpdateOverlay()
|
||||
}
|
||||
})
|
||||
})
|
||||
function clearActivationTimers() {
|
||||
window.clearTimeout(activationRetryTimer)
|
||||
window.clearTimeout(activationTimeoutTimer)
|
||||
activationRetryTimer = undefined
|
||||
activationTimeoutTimer = undefined
|
||||
}
|
||||
|
||||
function reloadForUpdate() {
|
||||
if (reloadStarted) return
|
||||
reloadStarted = true
|
||||
clearActivationTimers()
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function activateWaitingWorker(
|
||||
registration: ServiceWorkerRegistration | undefined,
|
||||
activateUpdate: (reloadPage?: boolean) => Promise<void>,
|
||||
showOverlay: boolean,
|
||||
) {
|
||||
if (activationInProgress) {
|
||||
if (showOverlay) showUpdateOverlay()
|
||||
return
|
||||
}
|
||||
activationInProgress = true
|
||||
|
||||
if (showOverlay) {
|
||||
showUpdateOverlay()
|
||||
}
|
||||
|
||||
registerSW({
|
||||
immediate: true,
|
||||
onOfflineReady() {
|
||||
console.log('📦 App offline ready')
|
||||
},
|
||||
// Workbox'ın controlling olayı herhangi bir nedenle kaçarsa native olay da
|
||||
// yeni worker kontrolü aldığı anda sayfayı yeniler.
|
||||
navigator.serviceWorker.addEventListener('controllerchange', reloadForUpdate, {
|
||||
once: true,
|
||||
})
|
||||
|
||||
// Workbox callback sırasına bağlı kalmadan waiting worker'ı doğrudan geçir.
|
||||
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
|
||||
|
||||
void activateUpdate(true).catch((error: unknown) => {
|
||||
console.error('Service worker activation failed.', error)
|
||||
activationInProgress = false
|
||||
clearActivationTimers()
|
||||
hideUpdateOverlay()
|
||||
})
|
||||
|
||||
// Bazı tarayıcılarda Workbox mesajı bekleyen worker'a ulaşmayabiliyor.
|
||||
// Kısa bir süre sonra native mesajla bir kez daha aktivasyon istenir.
|
||||
activationRetryTimer = window.setTimeout(() => {
|
||||
registration?.waiting?.postMessage({ type: 'SKIP_WAITING' })
|
||||
}, ACTIVATION_RETRY_DELAY)
|
||||
|
||||
// Aktivasyon yine de tamamlanamazsa uygulamayı sonsuza kadar kilitleme.
|
||||
// Eski worker çalışmaya devam eder; bir sonraki açılışta tekrar denenir.
|
||||
activationTimeoutTimer = window.setTimeout(() => {
|
||||
activationInProgress = false
|
||||
clearActivationTimers()
|
||||
|
||||
// Worker waiting durumundan çıktıysa aktivasyon gerçekleşmiş fakat
|
||||
// controllerchange olayı kaçmış olabilir. Yeni precache'i almak için yenile.
|
||||
if (!registration?.waiting && registration?.active) {
|
||||
reloadForUpdate()
|
||||
return
|
||||
}
|
||||
|
||||
hideUpdateOverlay()
|
||||
console.warn('Service worker activation timed out; continuing with current version.')
|
||||
}, ACTIVATION_TIMEOUT)
|
||||
}
|
||||
|
||||
function showUpdateWhileInstalling(registration: ServiceWorkerRegistration) {
|
||||
const installingWorker = registration.installing
|
||||
if (
|
||||
!installingWorker ||
|
||||
!navigator.serviceWorker.controller ||
|
||||
watchedInstallingWorkers.has(installingWorker)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
watchedInstallingWorkers.add(installingWorker)
|
||||
showUpdateOverlay()
|
||||
|
||||
installingWorker.addEventListener('statechange', () => {
|
||||
// Precache dosyalarından biri indirilemezse worker redundant olur. Eski
|
||||
// uygulama çalışmaya devam eder ve kullanıcı kilitli ekranda bırakılmaz.
|
||||
if (installingWorker.state === 'redundant' && !activationInProgress) {
|
||||
hideUpdateOverlay()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function watchForUpdate(registration: ServiceWorkerRegistration) {
|
||||
registration.addEventListener('updatefound', () => {
|
||||
// Gerçek worker güncellemesi bulunduğu anda, precache indirmesi başlamadan
|
||||
// Loading'i göster ve aktivasyon/reload tamamlanana kadar açık tut.
|
||||
showUpdateWhileInstalling(registration)
|
||||
})
|
||||
|
||||
// Listener bağlanmadan hemen önce başlamış bir kurulumu da kaçırma.
|
||||
showUpdateWhileInstalling(registration)
|
||||
}
|
||||
|
||||
function checkForUpdate(registration: ServiceWorkerRegistration) {
|
||||
// Bir deploy versiyon numarası/tag değişmeden yapılsa bile sw.js içindeki
|
||||
// precache manifest'i değişir. Uygulama açılırken yapılan bu tek kontrol
|
||||
// yeni build'i algılar; açık uygulamada periyodik bir arka plan işi çalışmaz.
|
||||
void registration.update().catch((error: unknown) => {
|
||||
console.warn('Service worker update check failed.', error)
|
||||
})
|
||||
}
|
||||
|
||||
export const registerServiceWorker = () => {
|
||||
if (registrationStarted || !('serviceWorker' in navigator)) return
|
||||
registrationStarted = true
|
||||
|
||||
registrationInitialization = navigator.serviceWorker
|
||||
.getRegistration()
|
||||
.catch(() => undefined)
|
||||
.then((existingRegistration) => {
|
||||
if (existingRegistration) {
|
||||
watchForUpdate(existingRegistration)
|
||||
}
|
||||
|
||||
const activateUpdate = registerSW({
|
||||
immediate: true,
|
||||
onRegisteredSW(_swUrl, registration) {
|
||||
if (!registration) return
|
||||
|
||||
if (registration !== existingRegistration) {
|
||||
watchForUpdate(registration)
|
||||
}
|
||||
currentRegistration = registration
|
||||
checkForUpdate(registration)
|
||||
|
||||
// waiting yalnızca aktif worker'dan farklı, kurulumu tamamlanmış gerçek
|
||||
// bir worker olduğunda bulunur. Onu doğrudan etkinleştir.
|
||||
if (registration.waiting && navigator.serviceWorker.controller) {
|
||||
activateWaitingWorker(registration, activateUpdate, true)
|
||||
}
|
||||
},
|
||||
onNeedRefresh() {
|
||||
// Bu callback worker'ın bütün yeni JS/CSS dosyalarını başarıyla precache'e
|
||||
// almasından sonra çalışır; aktivasyon ve reload atomik yapılır.
|
||||
activateWaitingWorker(currentRegistration, activateUpdate, true)
|
||||
},
|
||||
onNeedReload() {
|
||||
reloadForUpdate()
|
||||
},
|
||||
onOfflineReady() {
|
||||
console.log('📦 App offline ready')
|
||||
},
|
||||
onRegisterError(error) {
|
||||
activationInProgress = false
|
||||
clearActivationTimers()
|
||||
hideUpdateOverlay()
|
||||
console.error('Service worker registration failed.', error)
|
||||
},
|
||||
})
|
||||
activateUpdateHandler = activateUpdate
|
||||
})
|
||||
}
|
||||
|
||||
export const checkForAppUpdate = async (): Promise<ManualUpdateResult> => {
|
||||
if (!('serviceWorker' in navigator)) return 'unsupported'
|
||||
|
||||
// App içindeki normal kayıt işlemi henüz bitmediyse önce onu tamamla.
|
||||
await registrationInitialization
|
||||
const registration =
|
||||
currentRegistration ?? (await navigator.serviceWorker.ready.catch(() => undefined))
|
||||
|
||||
if (!registration) return 'unsupported'
|
||||
|
||||
let updateFound = false
|
||||
const handleUpdateFound = () => {
|
||||
updateFound = true
|
||||
}
|
||||
registration.addEventListener('updatefound', handleUpdateFound)
|
||||
try {
|
||||
await registration.update()
|
||||
} finally {
|
||||
registration.removeEventListener('updatefound', handleUpdateFound)
|
||||
}
|
||||
|
||||
const updateAvailable = Boolean(updateFound || registration.installing || registration.waiting)
|
||||
if (!updateAvailable) return 'up-to-date'
|
||||
|
||||
if (registration.waiting && activateUpdateHandler) {
|
||||
activateWaitingWorker(registration, activateUpdateHandler, true)
|
||||
}
|
||||
|
||||
// installing durumundaysa gerçek onNeedRefresh callback'i indirme bittikten
|
||||
// sonra aynı overlay/aktivasyon akışını başlatır.
|
||||
return 'updating'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,11 @@ export default defineConfig(async ({ mode }) => {
|
|||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
// Deploy'dan sonra otomatik güncelle
|
||||
registerType: 'autoUpdate',
|
||||
// Kayıt kodunu otomatik enjekte et (virtual:pwa-register yazmadan da çalışır)
|
||||
injectRegister: 'auto',
|
||||
// Yeni sürüm önce tamamen indirilir, ardından uygulama kontrollü olarak
|
||||
// yeni service worker'a geçer. Böylece eski ekran ile yeni chunk'lar karışmaz.
|
||||
registerType: 'prompt',
|
||||
// Kayıt ve loading akışı src/views/version/swRegistration.ts tarafından yönetilir.
|
||||
injectRegister: false,
|
||||
// Dev ortamında SW'yi aç, prod'da kapalı tut (build edilmiş SW prod'da zaten aktif olur)
|
||||
devOptions: {
|
||||
enabled: mode !== 'production',
|
||||
|
|
@ -38,9 +39,10 @@ export default defineConfig(async ({ mode }) => {
|
|||
// Büyük asset'leri de cache'leyebil
|
||||
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
|
||||
|
||||
// EN KRİTİK: yeni SW beklemeden kontrolü alsın
|
||||
// Yeni worker tüm precache dosyalarını indirdikten sonra uygulama
|
||||
// SKIP_WAITING mesajını gönderir. clientsClaim açık sekmeleri o anda yeniler.
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
skipWaiting: false,
|
||||
|
||||
// Eski workbox cache'lerini temizle
|
||||
cleanupOutdatedCaches: true,
|
||||
|
|
@ -48,14 +50,14 @@ export default defineConfig(async ({ mode }) => {
|
|||
// SPA fallback'i API çağrılarına uygulama
|
||||
navigateFallbackDenylist: [/^\/api\//],
|
||||
|
||||
// ⭐⭐ BU KISMI EKLEYİN: Cache sorununu çözecek runtime caching
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /\.(?:js|css|wasm)$/,
|
||||
handler: 'NetworkFirst',
|
||||
// Hash'li build dosyaları immutable'dır. Normal kullanımda ağ
|
||||
// beklenmez; deploy güncellemesini precache manifest'i yönetir.
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'static-resources-v2',
|
||||
networkTimeoutSeconds: 5,
|
||||
expiration: {
|
||||
maxEntries: 200,
|
||||
maxAgeSeconds: 30 * 24 * 60 * 60,
|
||||
|
|
@ -66,13 +68,14 @@ export default defineConfig(async ({ mode }) => {
|
|||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /\.json$/,
|
||||
// Changelog/versiyon bilgisi cache yüzünden eski kalmamalı.
|
||||
urlPattern: /\/version\.json$/,
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'json-resources-v1',
|
||||
networkTimeoutSeconds: 5,
|
||||
cacheName: 'app-version-v1',
|
||||
networkTimeoutSeconds: 3,
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxEntries: 5,
|
||||
maxAgeSeconds: 24 * 60 * 60,
|
||||
},
|
||||
cacheableResponse: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue