Compare commits
2
Commits
macos-v0.2.0
...
v0.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa8a0cc512 | ||
|
|
d18fffe665 |
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kanbangit-native",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -24,5 +24,5 @@
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.11.4"
|
||||
},
|
||||
"releaseNotes": "Update to full release v0.2.0"
|
||||
"releaseNotes": "Add the stable Mac download for the KanbanGit website."
|
||||
}
|
||||
|
||||
@@ -228,7 +228,7 @@ export async function publishMacos({
|
||||
await wait(2000);
|
||||
}
|
||||
}
|
||||
function assertUnpublishedRelease() {
|
||||
function assertUnpublishedRelease(allowSameVersion = false) {
|
||||
const file = join(staging, 'published-latest.json');
|
||||
try {
|
||||
s3('get-object', ['--key', latestKey, file]);
|
||||
@@ -261,7 +261,11 @@ export async function publishMacos({
|
||||
throw error;
|
||||
}
|
||||
const published = JSON.parse(readFileSync(file, 'utf8'));
|
||||
if (compareVersions(metadata.version, published.version) <= 0) {
|
||||
const comparison = compareVersions(metadata.version, published.version);
|
||||
if (allowSameVersion && comparison < 0) {
|
||||
throw new Error('Refusing to downgrade the stable download.');
|
||||
}
|
||||
if (comparison < 0 || (!allowSameVersion && comparison === 0)) {
|
||||
throw new Error(
|
||||
'This version has already been released through the update feed; bump the app version. Published objects were not overwritten.',
|
||||
);
|
||||
@@ -365,6 +369,55 @@ export async function publishMacos({
|
||||
);
|
||||
putReleaseFile('update.json', latest, 'application/json');
|
||||
await verifyPublic('update.json', latest);
|
||||
// An older release must not populate an absent download alias either.
|
||||
assertUnpublishedRelease(true);
|
||||
const downloadName = 'KanbanGit.dmg';
|
||||
const downloadKey = `${latestPrefix}/${downloadName}`;
|
||||
const downloadFile = join(staging, downloadName);
|
||||
writeFileSync(downloadFile, archive);
|
||||
function publishDownload(condition) {
|
||||
s3('put-object', [
|
||||
'--key',
|
||||
downloadKey,
|
||||
'--body',
|
||||
downloadFile,
|
||||
...condition,
|
||||
'--content-type',
|
||||
'application/x-apple-diskimage',
|
||||
'--cache-control',
|
||||
'no-cache, max-age=0, must-revalidate',
|
||||
'--content-md5',
|
||||
createHash('md5').update(archive).digest('base64'),
|
||||
'--metadata',
|
||||
`sha256=${checksum}`,
|
||||
]);
|
||||
}
|
||||
try {
|
||||
publishDownload(['--if-none-match', '*']);
|
||||
} catch (error) {
|
||||
if (!requiresExistingObjectCheck(error)) {
|
||||
throw error;
|
||||
}
|
||||
const previousFile = join(staging, 'previous-download.dmg');
|
||||
const response = JSON.parse(
|
||||
s3('get-object', [
|
||||
'--key',
|
||||
downloadKey,
|
||||
'--output',
|
||||
'json',
|
||||
previousFile,
|
||||
]),
|
||||
);
|
||||
if (sha256(readFileSync(previousFile)) !== checksum) {
|
||||
assertUnpublishedRelease();
|
||||
publishDownload(['--if-match', conditionalEtag(response.ETag)]);
|
||||
}
|
||||
}
|
||||
await verifyPublic(
|
||||
downloadName,
|
||||
archive,
|
||||
`${storage.publicBase}/${latestPrefix}`,
|
||||
);
|
||||
const latestFile = join(staging, 'latest.json');
|
||||
writeFileSync(latestFile, latest);
|
||||
function publishLatest(condition) {
|
||||
|
||||
Generated
+1
-1
@@ -1770,7 +1770,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kanbangit-native"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kanbangit-native"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
description = "KanbanGit native application"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KanbanGit",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"identifier": "de.kanbangit.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
@@ -25,7 +25,7 @@
|
||||
],
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "14.0",
|
||||
"bundleVersion": "6",
|
||||
"bundleVersion": "7",
|
||||
"dmg": {
|
||||
"windowSize": {
|
||||
"width": 660,
|
||||
|
||||
@@ -168,9 +168,9 @@ test('uploads revalidated files and writes completion record after public verifi
|
||||
);
|
||||
assert.equal(
|
||||
f.events.filter((event) => event.operation === 'put-object').length,
|
||||
8,
|
||||
9,
|
||||
);
|
||||
assert.equal(f.objects.size, 8);
|
||||
assert.equal(f.objects.size, 9);
|
||||
for (const event of f.events.filter(
|
||||
(event) => event.operation === 'put-object',
|
||||
)) {
|
||||
@@ -184,6 +184,26 @@ test('uploads revalidated files and writes completion record after public verifi
|
||||
f.events.at(-1).key,
|
||||
'releases/macos/aarch64-apple-darwin/latest.json',
|
||||
);
|
||||
const downloadKey = 'releases/macos/aarch64-apple-darwin/KanbanGit.dmg';
|
||||
assert.deepEqual(
|
||||
f.objects.get(downloadKey),
|
||||
readFileSync(join(f.directory, f.metadata.filename)),
|
||||
);
|
||||
const downloadPut = f.events.findIndex(
|
||||
(event) =>
|
||||
event.operation === 'put-object' && event.key === downloadKey,
|
||||
);
|
||||
const downloadVerified = f.events.findIndex(
|
||||
(event) =>
|
||||
event.operation === 'public-get' && event.key === downloadKey,
|
||||
);
|
||||
const feedPut = f.events.findIndex(
|
||||
(event) =>
|
||||
event.operation === 'put-object' &&
|
||||
event.key.endsWith('/latest.json'),
|
||||
);
|
||||
assert(downloadPut > 0 && downloadVerified > downloadPut);
|
||||
assert(feedPut > downloadVerified);
|
||||
assert.deepEqual(
|
||||
JSON.parse(
|
||||
f.objects.get(
|
||||
@@ -202,6 +222,91 @@ test('uploads revalidated files and writes completion record after public verifi
|
||||
);
|
||||
});
|
||||
|
||||
test('replaces the stable download only for a newer verified release', async (t) => {
|
||||
const f = fixture(t);
|
||||
const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`;
|
||||
const latestKey = `releases/macos/${f.metadata.target}/latest.json`;
|
||||
f.objects.set(downloadKey, Buffer.from('previous download'));
|
||||
f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.0.0' })));
|
||||
|
||||
await publishMacos(f);
|
||||
|
||||
assert.deepEqual(
|
||||
f.objects.get(downloadKey),
|
||||
readFileSync(join(f.directory, f.metadata.filename)),
|
||||
);
|
||||
assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.2.3');
|
||||
const replacement = f.events.find(
|
||||
(event) =>
|
||||
event.operation === 'put-object' &&
|
||||
event.key === downloadKey &&
|
||||
event.args.includes('--if-match'),
|
||||
);
|
||||
assert(replacement);
|
||||
});
|
||||
|
||||
test('a newer published version protects the stable download even when its alias is missing', async (t) => {
|
||||
const f = fixture(t);
|
||||
const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`;
|
||||
const latestKey = `releases/macos/${f.metadata.target}/latest.json`;
|
||||
f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '2.0.0' })));
|
||||
|
||||
await assert.rejects(publishMacos(f), /downgrade/);
|
||||
|
||||
assert.equal(f.objects.has(downloadKey), false);
|
||||
assert.equal(JSON.parse(f.objects.get(latestKey)).version, '2.0.0');
|
||||
});
|
||||
|
||||
test('a published version cannot replace a different stable download', async (t) => {
|
||||
const f = fixture(t);
|
||||
const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`;
|
||||
const latestKey = `releases/macos/${f.metadata.target}/latest.json`;
|
||||
const original = Buffer.from('published download');
|
||||
f.objects.set(downloadKey, original);
|
||||
f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.2.3' })));
|
||||
|
||||
await assert.rejects(publishMacos(f), /bump the app version/);
|
||||
|
||||
assert.deepEqual(f.objects.get(downloadKey), original);
|
||||
});
|
||||
|
||||
test('a failed public stable download blocks feed promotion', async (t) => {
|
||||
const f = fixture(t);
|
||||
const fetchPublic = f.fetchPublic;
|
||||
f.fetchPublic = (url, options) =>
|
||||
url.endsWith('/KanbanGit.dmg')
|
||||
? Promise.resolve({ ok: false, status: 403 })
|
||||
: fetchPublic(url, options);
|
||||
|
||||
await assert.rejects(publishMacos(f), /KanbanGit\.dmg: HTTP 403/);
|
||||
|
||||
assert.equal(
|
||||
f.objects.has(`releases/macos/${f.metadata.target}/latest.json`),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('concurrent stable download replacement preserves the winner', async (t) => {
|
||||
const f = fixture(t);
|
||||
const downloadKey = `releases/macos/${f.metadata.target}/KanbanGit.dmg`;
|
||||
const latestKey = `releases/macos/${f.metadata.target}/latest.json`;
|
||||
f.objects.set(downloadKey, Buffer.from('previous download'));
|
||||
f.objects.set(latestKey, Buffer.from(JSON.stringify({ version: '1.0.0' })));
|
||||
const run = f.run;
|
||||
f.run = (command, args) => {
|
||||
if (args.includes('--if-match') && args.includes(downloadKey)) {
|
||||
f.objects.set(downloadKey, Buffer.from('concurrent winner'));
|
||||
throw new Error('(PreconditionFailed) concurrent replacement');
|
||||
}
|
||||
return run(command, args);
|
||||
};
|
||||
|
||||
await assert.rejects(publishMacos(f), /concurrent replacement/);
|
||||
|
||||
assert.equal(f.objects.get(downloadKey).toString(), 'concurrent winner');
|
||||
assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.0.0');
|
||||
});
|
||||
|
||||
for (const response of ['', ' \n\t', '{}']) {
|
||||
test(`unversioned bucket response ${JSON.stringify(response)} permits a verified upload`, async (t) => {
|
||||
const f = fixture(t);
|
||||
@@ -211,7 +316,7 @@ for (const response of ['', ' \n\t', '{}']) {
|
||||
return args.includes('get-bucket-versioning') ? response : result;
|
||||
};
|
||||
await publishMacos(f);
|
||||
assert.equal(f.objects.size, 8);
|
||||
assert.equal(f.objects.size, 9);
|
||||
assert.equal(f.events.at(-1).operation, 'public-get');
|
||||
assert(f.events.at(-1).key.endsWith('/latest.json'));
|
||||
});
|
||||
@@ -237,7 +342,7 @@ test('retry accepts byte-identical objects without replacing them', async (t) =>
|
||||
assert.deepEqual([...f.objects.entries()], originals);
|
||||
assert.equal(
|
||||
f.events.filter((event) => event.operation === 'get-object').length,
|
||||
8,
|
||||
11,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -325,7 +430,7 @@ test('a partial upload can be retried with the same files', async (t) => {
|
||||
assert.equal(f.objects.size, 3);
|
||||
f.run = run;
|
||||
await publishMacos(f);
|
||||
assert.equal(f.objects.size, 8);
|
||||
assert.equal(f.objects.size, 9);
|
||||
});
|
||||
|
||||
function rebuild(f) {
|
||||
@@ -387,7 +492,7 @@ for (const previous of [null, '1.0.0']) {
|
||||
return result;
|
||||
};
|
||||
await publishMacos(f);
|
||||
assert.equal(f.objects.size, 8);
|
||||
assert.equal(f.objects.size, 9);
|
||||
assert.equal(JSON.parse(f.objects.get(latestKey)).version, '1.2.3');
|
||||
assert.equal(
|
||||
JSON.parse(f.objects.get(latestKey)).notes,
|
||||
@@ -516,7 +621,7 @@ for (const listing of [
|
||||
!('Contents' in listing)
|
||||
) {
|
||||
await publishMacos(f);
|
||||
assert.equal(f.objects.size, 8);
|
||||
assert.equal(f.objects.size, 9);
|
||||
} else {
|
||||
await assert.rejects(publishMacos(f), /NoneType|AccessDenied/);
|
||||
assert.equal(f.objects.get(key).toString(), 'unfinished old build');
|
||||
@@ -549,7 +654,7 @@ for (const status of [403, 404, 408, 429, 500, 502, 503, 504, 'network']) {
|
||||
await publishMacos(f);
|
||||
assert.deepEqual(waits, [2000, 2000]);
|
||||
assert.equal(cancelled, status === 'network' ? 0 : 2);
|
||||
assert.equal(f.objects.size, 8);
|
||||
assert.equal(f.objects.size, 9);
|
||||
assert(f.events.at(-1).key.endsWith('/latest.json'));
|
||||
});
|
||||
}
|
||||
@@ -593,7 +698,7 @@ test('checksum access retries finish before publishing the completion record', a
|
||||
};
|
||||
await publishMacos(f);
|
||||
assert.equal(failures, 3);
|
||||
assert.equal(f.objects.size, 8);
|
||||
assert.equal(f.objects.size, 9);
|
||||
});
|
||||
|
||||
for (const overrides of [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { Monitor, Moon, Plus, Sun } from 'lucide-vue-next';
|
||||
import { Download, Monitor, Moon, Plus, Sun } from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.vue';
|
||||
import UserMenuContent from '@/components/UserMenuContent.vue';
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '@/composables/useAppearance';
|
||||
import { useCurrentUrl } from '@/composables/useCurrentUrl';
|
||||
import { useLocalization } from '@/composables/useLocalization';
|
||||
import { useMacDownload } from '@/composables/useMacDownload';
|
||||
import { create as createProject } from '@/routes/projects';
|
||||
import { index as board } from '@/routes/projects/board';
|
||||
import { create as createTicket } from '@/routes/projects/tickets';
|
||||
@@ -35,6 +36,7 @@ const page = usePage();
|
||||
const { project } = useAppNavigation();
|
||||
const { isCurrentUrl } = useCurrentUrl();
|
||||
const { t } = useLocalization();
|
||||
const { macDownloadUrl, showMacDownload } = useMacDownload();
|
||||
const { appearance, updateAppearance } = useAppearance();
|
||||
const themeIcon = computed(
|
||||
() => ({ light: Sun, dark: Moon, system: Monitor })[appearance.value],
|
||||
@@ -69,6 +71,21 @@ const createAction = computed(() => {
|
||||
<span v-else class="truncate font-semibold">KanbanGit</span>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-1 sm:gap-2">
|
||||
<Button
|
||||
v-if="showMacDownload"
|
||||
as-child
|
||||
variant="outline"
|
||||
:aria-label="t('navigation.downloadAppAppleSilicon')"
|
||||
:title="t('navigation.downloadAppAppleSilicon')"
|
||||
class="px-3"
|
||||
>
|
||||
<a :href="macDownloadUrl"
|
||||
><Download aria-hidden="true" /><span
|
||||
class="hidden sm:inline"
|
||||
>{{ t('navigation.downloadApp') }}</span
|
||||
></a
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
v-if="createAction"
|
||||
as-child
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
import updateSource from '../../../native/update-source.json';
|
||||
|
||||
export const macDownloadUrl = `${updateSource.baseUrl}/aarch64-apple-darwin/KanbanGit.dmg`;
|
||||
|
||||
export function isMacDownloadAvailable(
|
||||
userAgent: string,
|
||||
maxTouchPoints: number,
|
||||
isTauri: unknown,
|
||||
): boolean {
|
||||
return (
|
||||
userAgent.includes('Macintosh') &&
|
||||
maxTouchPoints < 2 &&
|
||||
isTauri !== true
|
||||
);
|
||||
}
|
||||
|
||||
export function useMacDownload() {
|
||||
const showMacDownload = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
showMacDownload.value = isMacDownloadAvailable(
|
||||
navigator.userAgent,
|
||||
navigator.maxTouchPoints,
|
||||
Reflect.get(window, 'isTauri'),
|
||||
);
|
||||
});
|
||||
|
||||
return { macDownloadUrl, showMacDownload };
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import { ShieldCheck } from 'lucide-vue-next';
|
||||
import { Download, ShieldCheck } from 'lucide-vue-next';
|
||||
import type { Component } from 'vue';
|
||||
import AppLogo from '@/components/AppLogo.vue';
|
||||
import AppearanceTabs from '@/components/AppearanceTabs.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useLocalization } from '@/composables/useLocalization';
|
||||
import { useMacDownload } from '@/composables/useMacDownload';
|
||||
import { home } from '@/routes/index';
|
||||
|
||||
defineProps<{ title: string; description: string; icon?: Component }>();
|
||||
const { t } = useLocalization();
|
||||
const { macDownloadUrl, showMacDownload } = useMacDownload();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-background text-foreground flex min-h-dvh flex-col">
|
||||
<header
|
||||
class="bg-card px-page flex justify-center border-b py-4 sm:justify-start"
|
||||
class="bg-card px-page flex items-center justify-between gap-3 border-b py-4"
|
||||
>
|
||||
<Link
|
||||
:href="home()"
|
||||
@@ -22,6 +25,21 @@ const { t } = useLocalization();
|
||||
class="focus-visible:ring-ring/35 rounded-md outline-none focus-visible:ring-2"
|
||||
><AppLogo
|
||||
/></Link>
|
||||
<Button
|
||||
v-if="showMacDownload"
|
||||
as-child
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:aria-label="t('navigation.downloadAppAppleSilicon')"
|
||||
:title="t('navigation.downloadAppAppleSilicon')"
|
||||
>
|
||||
<a :href="macDownloadUrl"
|
||||
><Download aria-hidden="true" /><span
|
||||
class="hidden sm:inline"
|
||||
>{{ t('navigation.downloadApp') }}</span
|
||||
></a
|
||||
>
|
||||
</Button>
|
||||
</header>
|
||||
<main
|
||||
class="gap-column sm:px-page mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-4 py-8 sm:py-12"
|
||||
|
||||
@@ -718,6 +718,7 @@ export const en = {
|
||||
'welcome.action.dashboard': 'Open dashboard',
|
||||
'welcome.action.dashboardShort': 'Dashboard',
|
||||
'welcome.actionHint': 'One workspace for boards, tickets, and Git context.',
|
||||
'welcome.downloadAppHint': 'For Macs with Apple Silicon.',
|
||||
'welcome.preview.label': 'KanbanGit board preview',
|
||||
'welcome.preview.project': 'Product delivery board',
|
||||
'welcome.preview.connected': 'Git connected',
|
||||
@@ -899,6 +900,9 @@ export const en = {
|
||||
'settings.eyebrow': 'Account settings',
|
||||
'navigation.createTicket': 'New ticket',
|
||||
'navigation.createProject': 'New project',
|
||||
'navigation.downloadApp': 'Download App',
|
||||
'navigation.downloadAppAppleSilicon':
|
||||
'Download App for Macs with Apple Silicon',
|
||||
'navigation.open': 'Open navigation',
|
||||
'navigation.close': 'Close navigation',
|
||||
'navigation.expand': 'Expand sidebar',
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowRight, Columns3, GitBranch, ListChecks } from 'lucide-vue-next';
|
||||
import {
|
||||
ArrowRight,
|
||||
Columns3,
|
||||
Download,
|
||||
GitBranch,
|
||||
ListChecks,
|
||||
} from 'lucide-vue-next';
|
||||
import { computed } from 'vue';
|
||||
import AppLogo from '@/components/AppLogo.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useLocalization } from '@/composables/useLocalization';
|
||||
import { useMacDownload } from '@/composables/useMacDownload';
|
||||
import { dashboard, home, login } from '@/routes/index';
|
||||
|
||||
const { t } = useLocalization();
|
||||
const { macDownloadUrl, showMacDownload } = useMacDownload();
|
||||
|
||||
const features = computed(() => [
|
||||
{
|
||||
@@ -112,16 +120,38 @@ const previewColumns = computed(() => [
|
||||
{{ t('welcome.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button as-child size="lg">
|
||||
<Link v-if="$page.props.auth.user" :href="dashboard()">
|
||||
{{ t('welcome.action.dashboard') }}
|
||||
<ArrowRight aria-hidden="true" />
|
||||
</Link>
|
||||
<Link v-else :href="login()">
|
||||
{{ t('welcome.action.signIn') }}
|
||||
<ArrowRight aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
<div
|
||||
class="flex flex-wrap items-start justify-center gap-3"
|
||||
>
|
||||
<Button as-child size="lg">
|
||||
<Link
|
||||
v-if="$page.props.auth.user"
|
||||
:href="dashboard()"
|
||||
>
|
||||
{{ t('welcome.action.dashboard') }}
|
||||
<ArrowRight aria-hidden="true" />
|
||||
</Link>
|
||||
<Link v-else :href="login()">
|
||||
{{ t('welcome.action.signIn') }}
|
||||
<ArrowRight aria-hidden="true" />
|
||||
</Link>
|
||||
</Button>
|
||||
<div
|
||||
v-if="showMacDownload"
|
||||
class="grid justify-items-center gap-1"
|
||||
>
|
||||
<Button as-child size="lg" variant="outline">
|
||||
<a :href="macDownloadUrl"
|
||||
><Download aria-hidden="true" />{{
|
||||
t('navigation.downloadApp')
|
||||
}}</a
|
||||
>
|
||||
</Button>
|
||||
<p class="text-caption text-muted-foreground">
|
||||
{{ t('welcome.downloadAppHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-caption text-muted-foreground">
|
||||
{{ t('welcome.actionHint') }}
|
||||
</p>
|
||||
|
||||
@@ -90,10 +90,32 @@ if (serve) {
|
||||
const { en } = await server.ssrLoadModule(
|
||||
'/resources/js/locales/en.ts',
|
||||
);
|
||||
const { isMacDownloadAvailable, macDownloadUrl } =
|
||||
await server.ssrLoadModule(
|
||||
'/resources/js/composables/useMacDownload.ts',
|
||||
);
|
||||
const { default: Welcome } = await server.ssrLoadModule(
|
||||
'/resources/js/pages/Welcome.vue',
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
macDownloadUrl,
|
||||
'https://petsch-studios.nbg1.your-objectstorage.com/releases/macos/aarch64-apple-darwin/KanbanGit.dmg',
|
||||
);
|
||||
const macUserAgent =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15';
|
||||
assert.equal(isMacDownloadAvailable(macUserAgent, 0, undefined), true);
|
||||
assert.equal(isMacDownloadAvailable(macUserAgent, 0, true), false);
|
||||
assert.equal(isMacDownloadAvailable(macUserAgent, 5, undefined), false);
|
||||
assert.equal(
|
||||
isMacDownloadAvailable(
|
||||
'Mozilla/5.0 (Windows NT 10.0)',
|
||||
0,
|
||||
undefined,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
const render = async (url, localization = createLocalization()) => {
|
||||
const response = await createInertiaApp({
|
||||
page: makePage(url),
|
||||
|
||||
Reference in New Issue
Block a user