fix: restore macOS navigation with unsaved forms
CI/CD / preconditions (pull_request) Successful in 6s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 3m43s
macOS checks / Native Rust tests and unsigned build (pull_request) Canceled after 0s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 1m36s
CI/CD / preconditions (pull_request) Successful in 6s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 3m43s
macOS checks / Native Rust tests and unsigned build (pull_request) Canceled after 0s
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 1m36s
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kanbangit-native",
|
"name": "kanbangit-native",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -25,5 +25,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "2.11.4"
|
"@tauri-apps/cli": "2.11.4"
|
||||||
},
|
},
|
||||||
"releaseNotes": "Manage local workspace settings in the web app on macOS and iOS, with shared macOS update controls and native recovery for older or unavailable servers."
|
"releaseNotes": "Fix blocked navigation after changing project creation options or other forms on macOS by displaying native confirmations for unsaved changes."
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1
-1
@@ -1770,7 +1770,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kanbangit-native"
|
name = "kanbangit-native"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kanbangit-native"
|
name = "kanbangit-native"
|
||||||
version = "0.2.6"
|
version = "0.2.7"
|
||||||
description = "KanbanGit native application"
|
description = "KanbanGit native application"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
|||||||
@@ -467,7 +467,7 @@
|
|||||||
);
|
);
|
||||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
MARKETING_VERSION = 0.2.6;
|
MARKETING_VERSION = 0.2.7;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios;
|
||||||
PRODUCT_MODULE_NAME = KanbanGitNative;
|
PRODUCT_MODULE_NAME = KanbanGitNative;
|
||||||
PRODUCT_NAME = KanbanGit;
|
PRODUCT_NAME = KanbanGit;
|
||||||
@@ -644,7 +644,7 @@
|
|||||||
);
|
);
|
||||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||||
MARKETING_VERSION = 0.2.6;
|
MARKETING_VERSION = 0.2.7;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios;
|
||||||
PRODUCT_MODULE_NAME = KanbanGitNative;
|
PRODUCT_MODULE_NAME = KanbanGitNative;
|
||||||
PRODUCT_NAME = KanbanGit;
|
PRODUCT_NAME = KanbanGit;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ settingGroups:
|
|||||||
DEVELOPMENT_TEAM: ZZX2KZGTHU
|
DEVELOPMENT_TEAM: ZZX2KZGTHU
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING: false
|
ENABLE_USER_SCRIPT_SANDBOXING: false
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.ios
|
PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.ios
|
||||||
MARKETING_VERSION: "0.2.6"
|
MARKETING_VERSION: "0.2.7"
|
||||||
CURRENT_PROJECT_VERSION: "6"
|
CURRENT_PROJECT_VERSION: "6"
|
||||||
configs:
|
configs:
|
||||||
debug:
|
debug:
|
||||||
|
|||||||
@@ -1,6 +1,55 @@
|
|||||||
|
#import <AppKit/AppKit.h>
|
||||||
#import <WebKit/WebKit.h>
|
#import <WebKit/WebKit.h>
|
||||||
|
#import <objc/runtime.h>
|
||||||
|
|
||||||
|
@interface KanbanWorkspaceUIDelegate : NSObject <WKUIDelegate>
|
||||||
|
@property(nonatomic, strong) id<WKUIDelegate> originalDelegate;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation KanbanWorkspaceUIDelegate
|
||||||
|
- (BOOL)respondsToSelector:(SEL)selector {
|
||||||
|
return [super respondsToSelector:selector] || [self.originalDelegate respondsToSelector:selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)forwardingTargetForSelector:(SEL)selector {
|
||||||
|
if ([self.originalDelegate respondsToSelector:selector]) {
|
||||||
|
return self.originalDelegate;
|
||||||
|
}
|
||||||
|
return [super forwardingTargetForSelector:selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message
|
||||||
|
initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
|
||||||
|
NSWindow *window = webView.window;
|
||||||
|
if (!window || window.attachedSheet) {
|
||||||
|
completionHandler(NO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSAlert *alert = [NSAlert new];
|
||||||
|
alert.alertStyle = NSAlertStyleWarning;
|
||||||
|
alert.messageText = frame.request.URL.host ?: @"KanbanGit";
|
||||||
|
alert.informativeText = message;
|
||||||
|
[alert addButtonWithTitle:@"OK"];
|
||||||
|
[alert addButtonWithTitle:@"Cancel"];
|
||||||
|
[alert beginSheetModalForWindow:window completionHandler:^(NSModalResponse response) {
|
||||||
|
completionHandler(response == NSAlertFirstButtonReturn);
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
static char workspaceUIDelegateKey;
|
||||||
|
|
||||||
void install_workspace_navigation(void *pointer) {
|
void install_workspace_navigation(void *pointer) {
|
||||||
WKWebView *webview = (__bridge WKWebView *)pointer;
|
WKWebView *webview = (__bridge WKWebView *)pointer;
|
||||||
webview.allowsBackForwardNavigationGestures = YES;
|
webview.allowsBackForwardNavigationGestures = YES;
|
||||||
|
if ([webview.UIDelegate isKindOfClass:KanbanWorkspaceUIDelegate.class]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KanbanWorkspaceUIDelegate *delegate = [KanbanWorkspaceUIDelegate new];
|
||||||
|
delegate.originalDelegate = webview.UIDelegate;
|
||||||
|
// WKWebView keeps a weak UI delegate, so retain the forwarding adapter with its view.
|
||||||
|
objc_setAssociatedObject(webview, &workspaceUIDelegateKey, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||||
|
webview.UIDelegate = delegate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "KanbanGit",
|
"productName": "KanbanGit",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"identifier": "de.kanbangit.desktop",
|
"identifier": "de.kanbangit.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
],
|
],
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"minimumSystemVersion": "14.0",
|
"minimumSystemVersion": "14.0",
|
||||||
"bundleVersion": "11",
|
"bundleVersion": "12",
|
||||||
"dmg": {
|
"dmg": {
|
||||||
"windowSize": {
|
"windowSize": {
|
||||||
"width": 660,
|
"width": 660,
|
||||||
|
|||||||
@@ -17,6 +17,53 @@ static void waitUntil(BOOL (^condition)(void), NSString *message) {
|
|||||||
check(condition(), message);
|
check(condition(), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static id evaluate(WKWebView *webview, NSString *script) {
|
||||||
|
__block BOOL finished = NO;
|
||||||
|
__block id result;
|
||||||
|
[webview evaluateJavaScript:script completionHandler:^(id value, NSError *error) {
|
||||||
|
check(error == nil, error.localizedDescription);
|
||||||
|
result = value;
|
||||||
|
finished = YES;
|
||||||
|
}];
|
||||||
|
waitUntil(^BOOL { return finished; }, @"JavaScript must finish after the confirmation is dismissed");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSButton *findButton(NSView *view, NSString *title) {
|
||||||
|
if ([view isKindOfClass:NSButton.class] && [((NSButton *)view).title isEqualToString:title]) {
|
||||||
|
return (NSButton *)view;
|
||||||
|
}
|
||||||
|
for (NSView *child in view.subviews) {
|
||||||
|
NSButton *button = findButton(child, title);
|
||||||
|
if (button) { return button; }
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL containsText(NSView *view, NSString *text) {
|
||||||
|
if ([view isKindOfClass:NSTextField.class] && [((NSTextField *)view).stringValue isEqualToString:text]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
for (NSView *child in view.subviews) {
|
||||||
|
if (containsText(child, text)) { return YES; }
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface WorkspaceUI : NSObject <WKUIDelegate>
|
||||||
|
@property(nonatomic) BOOL closed;
|
||||||
|
@property(nonatomic) BOOL uploaded;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation WorkspaceUI
|
||||||
|
- (void)webViewDidClose:(WKWebView *)webView { self.closed = YES; }
|
||||||
|
- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters
|
||||||
|
initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray<NSURL *> *))completionHandler {
|
||||||
|
self.uploaded = YES;
|
||||||
|
completionHandler(nil);
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
@interface WorkspacePages : NSObject <WKURLSchemeHandler, WKNavigationDelegate>
|
@interface WorkspacePages : NSObject <WKURLSchemeHandler, WKNavigationDelegate>
|
||||||
@property(nonatomic) NSUInteger finished;
|
@property(nonatomic) NSUInteger finished;
|
||||||
@end
|
@end
|
||||||
@@ -46,9 +93,18 @@ int main(void) {
|
|||||||
[configuration setURLSchemeHandler:pages forURLScheme:@"kanban-test"];
|
[configuration setURLSchemeHandler:pages forURLScheme:@"kanban-test"];
|
||||||
WKWebView *webview = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration];
|
WKWebView *webview = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration];
|
||||||
webview.navigationDelegate = pages;
|
webview.navigationDelegate = pages;
|
||||||
|
WorkspaceUI *originalUI = [WorkspaceUI new];
|
||||||
|
webview.UIDelegate = originalUI;
|
||||||
check(!webview.allowsBackForwardNavigationGestures, @"WebKit defaults to disabled gestures");
|
check(!webview.allowsBackForwardNavigationGestures, @"WebKit defaults to disabled gestures");
|
||||||
install_workspace_navigation((__bridge void *)webview);
|
install_workspace_navigation((__bridge void *)webview);
|
||||||
|
id<WKUIDelegate> installedUI = webview.UIDelegate;
|
||||||
install_workspace_navigation((__bridge void *)webview);
|
install_workspace_navigation((__bridge void *)webview);
|
||||||
|
check(webview.UIDelegate == installedUI, @"Installing workspace behavior twice must retain the same delegate");
|
||||||
|
check([installedUI respondsToSelector:@selector(webViewDidClose:)], @"Existing popup callbacks must remain available");
|
||||||
|
[installedUI webViewDidClose:webview];
|
||||||
|
check(originalUI.closed, @"Existing popup callbacks must reach the original delegate");
|
||||||
|
check([installedUI respondsToSelector:@selector(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:)],
|
||||||
|
@"Existing file upload callbacks must remain available");
|
||||||
check(webview.allowsBackForwardNavigationGestures, @"The workspace must enable WebKit's interactive gestures");
|
check(webview.allowsBackForwardNavigationGestures, @"The workspace must enable WebKit's interactive gestures");
|
||||||
check(webview.configuration.userContentController.userScripts.count == 0, @"Enabling gestures must not inject a history adapter");
|
check(webview.configuration.userContentController.userScripts.count == 0, @"Enabling gestures must not inject a history adapter");
|
||||||
check([webview goBack] == nil && [webview goForward] == nil, @"Empty history must not navigate");
|
check([webview goBack] == nil && [webview goForward] == nil, @"Empty history must not navigate");
|
||||||
@@ -69,6 +125,41 @@ int main(void) {
|
|||||||
check([webview.URL.path isEqualToString:@"/second"] && webview.canGoBack && !webview.canGoForward,
|
check([webview.URL.path isEqualToString:@"/second"] && webview.canGoBack && !webview.canGoForward,
|
||||||
@"Forward must restore the second page");
|
@"Forward must restore the second page");
|
||||||
check(webview.allowsBackForwardNavigationGestures, @"Native gestures must remain enabled across page navigation");
|
check(webview.allowsBackForwardNavigationGestures, @"Native gestures must remain enabled across page navigation");
|
||||||
puts("PASS: WebKit interactive gesture configuration, full-page back/forward and empty history");
|
|
||||||
|
NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600)
|
||||||
|
styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:NO];
|
||||||
|
window.releasedWhenClosed = NO;
|
||||||
|
window.title = @"KanbanGit navigation test";
|
||||||
|
window.contentView = webview;
|
||||||
|
[window orderFront:nil];
|
||||||
|
evaluate(webview, @"document.body.innerHTML = `<input id='duplicate' type='radio'><input id='draft' value='Unsaved project'><input id='upload' type='file'><a id='back' href='#back'>Back</a>`; window.visits = 0; document.querySelector('#back').onclick = event => { event.preventDefault(); if (!document.querySelector('#duplicate').checked || confirm('Discard unsaved changes?')) window.visits++; }; true;");
|
||||||
|
evaluate(webview, @"document.querySelector('#upload').click()");
|
||||||
|
waitUntil(^BOOL { return originalUI.uploaded; }, @"File selection must still reach the original UI delegate");
|
||||||
|
evaluate(webview, @"document.querySelector('#back').click()");
|
||||||
|
check([evaluate(webview, @"window.visits") intValue] == 1, @"Pristine forms must navigate without a confirmation");
|
||||||
|
evaluate(webview, @"document.querySelector('#duplicate').click()");
|
||||||
|
for (NSString *choice in @[@"Cancel", @"OK", @"Cancel", @"OK"]) {
|
||||||
|
__block BOOL finished = NO;
|
||||||
|
[webview evaluateJavaScript:@"document.querySelector('#back').click()" completionHandler:^(id result, NSError *error) {
|
||||||
|
check(error == nil, error.localizedDescription);
|
||||||
|
finished = YES;
|
||||||
|
}];
|
||||||
|
waitUntil(^BOOL { return window.attachedSheet != nil; }, @"Dirty form navigation must show a native confirmation");
|
||||||
|
check(!finished, @"The original click must wait for the user's decision");
|
||||||
|
check(containsText(window.attachedSheet.contentView, @"workspace"), @"The confirmation must identify the requesting website");
|
||||||
|
check(containsText(window.attachedSheet.contentView, @"Discard unsaved changes?"), @"The confirmation must show the application's message");
|
||||||
|
NSButton *button = findButton(window.attachedSheet.contentView, choice);
|
||||||
|
check(button != nil, @"The native confirmation must offer OK and Cancel");
|
||||||
|
[button performClick:nil];
|
||||||
|
waitUntil(^BOOL { return finished && window.attachedSheet == nil; }, @"Dismissal must release the original navigation");
|
||||||
|
check([evaluate(webview, @"document.querySelector('#draft').value") isEqualToString:@"Unsaved project"],
|
||||||
|
@"Cancelling a confirmation must preserve the draft");
|
||||||
|
}
|
||||||
|
check([evaluate(webview, @"window.visits") intValue] == 3, @"Only accepted visits must run, exactly once each");
|
||||||
|
window.contentView = nil;
|
||||||
|
check(![evaluate(webview, @"confirm('Detached view')") boolValue], @"A detached view must cancel and release JavaScript immediately");
|
||||||
|
[window orderOut:nil];
|
||||||
|
[window close];
|
||||||
|
puts("PASS: WebKit interactive gesture configuration, full-page back/forward, native confirmations and delegate forwarding");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import test from 'node:test';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'WebKit owns interactive navigation and preserves native back/forward history',
|
'WebKit preserves history and confirms dirty form navigation without losing delegate callbacks',
|
||||||
{ skip: process.platform !== 'darwin' },
|
{ skip: process.platform !== 'darwin' },
|
||||||
() => {
|
() => {
|
||||||
const directory = mkdtempSync(join(tmpdir(), 'kanbangit-navigation-'));
|
const directory = mkdtempSync(join(tmpdir(), 'kanbangit-navigation-'));
|
||||||
@@ -48,7 +48,7 @@ test(
|
|||||||
assert.equal(
|
assert.equal(
|
||||||
result.status,
|
result.status,
|
||||||
0,
|
0,
|
||||||
result.stderr || result.error?.message,
|
result.stderr || result.error?.message || result.signal,
|
||||||
);
|
);
|
||||||
assert.match(result.stdout, /PASS: WebKit/);
|
assert.match(result.stdout, /PASS: WebKit/);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user