Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Plugin system architecture — all 8 database drivers (MySQL, PostgreSQL, SQLite, ClickHouse, MSSQL, MongoDB, Redis, Oracle) extracted into `.tableplugin` bundles loaded at runtime
- Export format plugins — all 5 export formats (CSV, JSON, SQL, XLSX, MQL) extracted into `.tableplugin` bundles with plugin-provided option views and per-table option columns
- Settings > Plugins tab for plugin management — list installed plugins, enable/disable, install from file, uninstall user plugins, view plugin details
- Plugin marketplace — browse, search, and install plugins from the GitHub-hosted registry with SHA-256 checksum verification, ETag caching, and offline fallback
- TableProPluginKit framework — shared protocols and types for driver plugins
- TableProPluginKit framework — shared protocols and types for driver and export plugins
- ClickHouse database support with query progress tracking, EXPLAIN variants, TLS/HTTPS, server-side cancellation, and Parts view

### Changed
Expand Down
82 changes: 82 additions & 0 deletions Plugins/CSVExportPlugin/CSVExportModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// CSVExportModels.swift
// CSVExportPlugin
//

import Foundation

public enum CSVDelimiter: String, CaseIterable, Identifiable {
case comma = ","
case semicolon = ";"
case tab = "\\t"
case pipe = "|"

public var id: String { rawValue }

public var displayName: String {
switch self {
case .comma: return ","
case .semicolon: return ";"
case .tab: return "\\t"
case .pipe: return "|"
}
}

public var actualValue: String {
self == .tab ? "\t" : rawValue
}
}

public enum CSVQuoteHandling: String, CaseIterable, Identifiable {
case always = "Always"
case asNeeded = "Quote if needed"
case never = "Never"

public var id: String { rawValue }

public var displayName: String {
switch self {
case .always: return String(localized: "Always", bundle: .main)
case .asNeeded: return String(localized: "Quote if needed", bundle: .main)
case .never: return String(localized: "Never", bundle: .main)
}
}
}

public enum CSVLineBreak: String, CaseIterable, Identifiable {
case lf = "\\n"
case crlf = "\\r\\n"
case cr = "\\r"

public var id: String { rawValue }

public var value: String {
switch self {
case .lf: return "\n"
case .crlf: return "\r\n"
case .cr: return "\r"
}
}
}

public enum CSVDecimalFormat: String, CaseIterable, Identifiable {
case period = "."
case comma = ","

public var id: String { rawValue }

public var separator: String { rawValue }
}

public struct CSVExportOptions: Equatable {
public var convertNullToEmpty: Bool = true
public var convertLineBreakToSpace: Bool = false
public var includeFieldNames: Bool = true
public var delimiter: CSVDelimiter = .comma
public var quoteHandling: CSVQuoteHandling = .asNeeded
public var lineBreak: CSVLineBreak = .lf
public var decimalFormat: CSVDecimalFormat = .period
public var sanitizeFormulas: Bool = true

public init() {}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,36 @@
//
// ExportCSVOptionsView.swift
// TablePro
//
// Options panel for CSV export format.
// Provides controls for delimiter, quoting, NULL handling, and formatting.
// CSVExportOptionsView.swift
// CSVExportPlugin
//

import SwiftUI

/// Options panel for CSV export
struct ExportCSVOptionsView: View {
@Binding var options: CSVExportOptions
struct CSVExportOptionsView: View {
@Bindable var plugin: CSVExportPlugin

var body: some View {
VStack(alignment: .leading, spacing: 10) {
// Checkboxes section
VStack(alignment: .leading, spacing: 8) {
Toggle("Convert NULL to EMPTY", isOn: $options.convertNullToEmpty)
Toggle("Convert NULL to EMPTY", isOn: $plugin.options.convertNullToEmpty)
.toggleStyle(.checkbox)

Toggle("Convert line break to space", isOn: $options.convertLineBreakToSpace)
Toggle("Convert line break to space", isOn: $plugin.options.convertLineBreakToSpace)
.toggleStyle(.checkbox)

Toggle("Put field names in the first row", isOn: $options.includeFieldNames)
Toggle("Put field names in the first row", isOn: $plugin.options.includeFieldNames)
.toggleStyle(.checkbox)

Toggle("Sanitize formula-like values", isOn: $options.sanitizeFormulas)
Toggle("Sanitize formula-like values", isOn: $plugin.options.sanitizeFormulas)
.toggleStyle(.checkbox)
.help("Prevent CSV formula injection by prefixing values starting with =, +, -, @ with a single quote")
}

Divider()
.padding(.vertical, 4)

// Dropdowns section
VStack(alignment: .leading, spacing: 10) {
optionRow(String(localized: "Delimiter")) {
Picker("", selection: $options.delimiter) {
optionRow(String(localized: "Delimiter", bundle: .main)) {
Picker("", selection: $plugin.options.delimiter) {
ForEach(CSVDelimiter.allCases) { delimiter in
Text(delimiter.displayName).tag(delimiter)
}
Expand All @@ -46,8 +40,8 @@ struct ExportCSVOptionsView: View {
.frame(width: 140, alignment: .trailing)
}

optionRow(String(localized: "Quote")) {
Picker("", selection: $options.quoteHandling) {
optionRow(String(localized: "Quote", bundle: .main)) {
Picker("", selection: $plugin.options.quoteHandling) {
ForEach(CSVQuoteHandling.allCases) { handling in
Text(handling.rawValue).tag(handling)
}
Expand All @@ -57,8 +51,8 @@ struct ExportCSVOptionsView: View {
.frame(width: 140, alignment: .trailing)
}

optionRow(String(localized: "Line break")) {
Picker("", selection: $options.lineBreak) {
optionRow(String(localized: "Line break", bundle: .main)) {
Picker("", selection: $plugin.options.lineBreak) {
ForEach(CSVLineBreak.allCases) { lineBreak in
Text(lineBreak.rawValue).tag(lineBreak)
}
Expand All @@ -68,8 +62,8 @@ struct ExportCSVOptionsView: View {
.frame(width: 140, alignment: .trailing)
}

optionRow(String(localized: "Decimal")) {
Picker("", selection: $options.decimalFormat) {
optionRow(String(localized: "Decimal", bundle: .main)) {
Picker("", selection: $plugin.options.decimalFormat) {
ForEach(CSVDecimalFormat.allCases) { format in
Text(format.rawValue).tag(format)
}
Expand All @@ -96,11 +90,3 @@ struct ExportCSVOptionsView: View {
}
}
}

// MARK: - Preview

#Preview {
ExportCSVOptionsView(options: .constant(CSVExportOptions()))
.padding()
.frame(width: 280)
}
Loading