Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Remote editor exceptions #109

Merged
merged 28 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1fe50bc
fix: Open user-initiated link navigation in OS browser
dcalhoun Mar 3, 2025
c8e1185
refactor: Update preloaded data comments
dcalhoun Mar 3, 2025
fbdfbef
fix: Exclude remote scripts by ID rather than path
dcalhoun Mar 3, 2025
b329ad9
feat: Allow multiple siteApiNamespace values
dcalhoun Mar 4, 2025
0452dd9
feat: Editor assets request utilizes site configuration
dcalhoun Mar 4, 2025
0da1b48
feat: Allow excluding API paths from API namespacing
dcalhoun Mar 4, 2025
8a40965
fix: Limit WebView navigation to allow list
dcalhoun Mar 7, 2025
c1b4b36
feat: Allowing host app setting JavaScript globals
dcalhoun Mar 7, 2025
d4fbf22
feat: Allow setting both bundled and site-specific editor URLs
dcalhoun Mar 24, 2025
bd97e67
feat: Android loads dedicated remote editor URL for plugin support
dcalhoun Mar 25, 2025
32a5621
feat: Android siteApiNamespace accepts multiple strings
dcalhoun Mar 25, 2025
39d660f
feat: Android allows excluding paths from namespacing
dcalhoun Mar 25, 2025
9585f34
refactor: Android utilizes EditorConfiguration class
dcalhoun Mar 25, 2025
4f55f2e
feat: Allow setting global properties within the Android editor WebView
dcalhoun Mar 25, 2025
a0ae338
refactor: Set Android global editor configuration on page start
dcalhoun Mar 25, 2025
ab566a3
task: Capture build output
dcalhoun Mar 25, 2025
56c0e14
fix: Ensure inline CSS is not externalized
dcalhoun Mar 25, 2025
03bf822
feat: Android loads remote editor bundle URL
dcalhoun Mar 26, 2025
f263817
task: Capture build output
dcalhoun Mar 26, 2025
d090d36
fix: Parcelize Android web view globals
dcalhoun Mar 27, 2025
60efc7a
feat: Expand URLs managed by the Android WebView
dcalhoun Mar 27, 2025
e8b0b86
task: Makefile allows skipping dependency installation
dcalhoun Mar 28, 2025
8144e7b
fix: Verify GBKit global configuration presence before initializing
dcalhoun Mar 28, 2025
b36a13e
refactor: Tidy working code
dcalhoun Mar 28, 2025
07fdeca
feat: Improve editor load error handling
dcalhoun Mar 28, 2025
62e3f0d
task: Capture build output
dcalhoun Mar 28, 2025
56d7b13
task: Update Android example app configuration
dcalhoun Mar 28, 2025
ffd2e12
docs: Add inline comments
dcalhoun Mar 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

SIMULATOR_DESTINATION := OS=17.5,name=iPhone 15 Plus

define XCODEBUILD_CMD
Expand All @@ -11,8 +10,10 @@ define XCODEBUILD_CMD
endef

npm-dependencies:
echo "--- :npm: Installing NPM Dependencies"
npm ci
@if [ "$(SKIP_DEPS)" != "true" ]; then \
echo "--- :npm: Installing NPM Dependencies"; \
npm ci; \
fi

build: npm-dependencies
echo "--- :node: Building Gutenberg"
Expand Down
9 changes: 8 additions & 1 deletion android/Gutenberg/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.jetbrains.kotlin.android)
id("com.automattic.android.publish-to-s3")
id("kotlin-parcelize")
}

android {
Expand All @@ -21,6 +22,12 @@ android {
"\"${rootProject.ext["gutenbergEditorUrl"] ?: ""}\""
)

buildConfigField(
"String",
"GUTENBERG_EDITOR_REMOTE_URL",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add GUTENBERG_EDITOR_REMOTE_URL to simplify running both the local (bundled) and remote (site-specific) editor servers at the same time.

"\"${rootProject.ext["gutenbergEditorRemoteUrl"] ?: ""}\""
)

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
Expand Down Expand Up @@ -68,4 +75,4 @@ project.afterEvaluate {
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package org.wordpress.gutenberg

import android.os.Parcelable
import android.os.Parcel
import kotlinx.parcelize.Parcelize

@Parcelize
sealed class WebViewGlobalValue : Parcelable {
@Parcelize
data class StringValue(val value: String) : WebViewGlobalValue()
@Parcelize
data class NumberValue(val value: Double) : WebViewGlobalValue()
@Parcelize
data class BooleanValue(val value: Boolean) : WebViewGlobalValue()
@Parcelize
data class ObjectValue(val value: Map<String, WebViewGlobalValue>) : WebViewGlobalValue()
@Parcelize
data class ArrayValue(val value: List<WebViewGlobalValue>) : WebViewGlobalValue()
@Parcelize
object NullValue : WebViewGlobalValue()

fun toJavaScript(): String {
return when (this) {
is StringValue -> "\"${value.replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")}\""
is NumberValue -> value.toString()
is BooleanValue -> value.toString()
is ObjectValue -> {
val pairs = value.map { (key, value) -> "\"$key\": ${value.toJavaScript()}" }
"{${pairs.joinToString(",")}}"
}
is ArrayValue -> "[${value.joinToString(",") { it.toJavaScript() }}]"
is NullValue -> "null"
}
}
}

@Parcelize
data class WebViewGlobal(
val name: String,
val value: WebViewGlobalValue
) : Parcelable {
init {
require(name.matches(Regex("^[a-zA-Z_$][a-zA-Z0-9_$]*$"))) {
"Invalid JavaScript identifier: $name"
}
}
}

@Parcelize
open class EditorConfiguration constructor(
val title: String,
val content: String,
val postId: Int?,
val postType: String?,
val themeStyles: Boolean,
val plugins: Boolean,
val hideTitle: Boolean,
val siteURL: String,
val siteApiRoot: String,
val siteApiNamespace: Array<String>,
val namespaceExcludedPaths: Array<String>,
val authHeader: String,
val webViewGlobals: List<WebViewGlobal>
) : Parcelable {
companion object {
@JvmStatic
fun builder(): Builder = Builder()
}

class Builder {
private var title: String = ""
private var content: String = ""
private var postId: Int? = null
private var postType: String? = null
private var themeStyles: Boolean = false
private var plugins: Boolean = false
private var hideTitle: Boolean = false
private var siteURL: String = ""
private var siteApiRoot: String = ""
private var siteApiNamespace: Array<String> = arrayOf()
private var namespaceExcludedPaths: Array<String> = arrayOf()
private var authHeader: String = ""
private var webViewGlobals: List<WebViewGlobal> = emptyList()

fun setTitle(title: String) = apply { this.title = title }
fun setContent(content: String) = apply { this.content = content }
fun setPostId(postId: Int?) = apply { this.postId = postId }
fun setPostType(postType: String?) = apply { this.postType = postType }
fun setThemeStyles(themeStyles: Boolean) = apply { this.themeStyles = themeStyles }
fun setPlugins(plugins: Boolean) = apply { this.plugins = plugins }
fun setHideTitle(hideTitle: Boolean) = apply { this.hideTitle = hideTitle }
fun setSiteURL(siteURL: String) = apply { this.siteURL = siteURL }
fun setSiteApiRoot(siteApiRoot: String) = apply { this.siteApiRoot = siteApiRoot }
fun setSiteApiNamespace(siteApiNamespace: Array<String>) = apply { this.siteApiNamespace = siteApiNamespace }
fun setNamespaceExcludedPaths(namespaceExcludedPaths: Array<String>) = apply { this.namespaceExcludedPaths = namespaceExcludedPaths }
fun setAuthHeader(authHeader: String) = apply { this.authHeader = authHeader }
fun setWebViewGlobals(webViewGlobals: List<WebViewGlobal>) = apply { this.webViewGlobals = webViewGlobals }

fun build(): EditorConfiguration = EditorConfiguration(
title = title,
content = content,
postId = postId,
postType = postType,
themeStyles = themeStyles,
plugins = plugins,
hideTitle = hideTitle,
siteURL = siteURL,
siteApiRoot = siteApiRoot,
siteApiNamespace = siteApiNamespace,
namespaceExcludedPaths = namespaceExcludedPaths,
authHeader = authHeader,
webViewGlobals = webViewGlobals
)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as EditorConfiguration

if (title != other.title) return false
if (content != other.content) return false
if (postId != other.postId) return false
if (postType != other.postType) return false
if (themeStyles != other.themeStyles) return false
if (plugins != other.plugins) return false
if (hideTitle != other.hideTitle) return false
if (siteURL != other.siteURL) return false
if (siteApiRoot != other.siteApiRoot) return false
if (!siteApiNamespace.contentEquals(other.siteApiNamespace)) return false
if (!namespaceExcludedPaths.contentEquals(other.namespaceExcludedPaths)) return false
if (authHeader != other.authHeader) return false
if (webViewGlobals != other.webViewGlobals) return false

return true
}

override fun hashCode(): Int {
var result = title.hashCode()
result = 31 * result + content.hashCode()
result = 31 * result + (postId ?: 0)
result = 31 * result + (postType?.hashCode() ?: 0)
result = 31 * result + themeStyles.hashCode()
result = 31 * result + plugins.hashCode()
result = 31 * result + hideTitle.hashCode()
result = 31 * result + siteURL.hashCode()
result = 31 * result + siteApiRoot.hashCode()
result = 31 * result + siteApiNamespace.contentHashCode()
result = 31 * result + namespaceExcludedPaths.contentHashCode()
result = 31 * result + authHeader.hashCode()
result = 31 * result + webViewGlobals.hashCode()
return result
}
}
Loading