Misc · Export and import: export, field mapping and a CSV importer

misc::ExportDialog offers the owner's formats as cards and hands the
chosen one over for the owner to write. misc::ImportDialog maps the
file's columns to the app's fields, matched by name to start, over a
preview, and asks for each required field's column. misc::CsvImporter
reads CSV as RFC 4180, lets the first row name the columns or not, and
maps it through the ImportDialog into records; text that does not read
says why.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-28 05:20:03 -04:00
1 parent cbba4ddeec
commit 79fe78cefd
9 files changed
+1023 -4

No files matched your search

+11 -1
View File
@@ -10,13 +10,14 @@ use crate::ui::{section, specimen, specimens};
mod asking;
mod codes;
mod notices;
mod transfer;
mod web;
pub const PAGE: Page = Page {
number: 42,
slug: "misc",
title: "Misc",
summary: "Small tools that stand alone: clocks, a stopwatch, a calculator, a unit converter, a QR code scanner, a captcha, a poll, a survey, a quiz, flashcards, licenses, terms, a cookie banner and web views.",
summary: "Small tools that stand alone: clocks, a stopwatch, a calculator, a unit converter, a QR code scanner, a captcha, a poll, a survey, a quiz, flashcards, licenses, terms, a cookie banner, data out and in, and web views.",
render,
script: &[
Step::DownAt("stopwatch-start", 30.0, 62.0),
@@ -66,6 +67,14 @@ pub const PAGE: Page = Page {
Step::UpAt("cookies", 230.0, 84.0),
Step::HoverAt("cookies", 20.0, 20.0),
Step::Shot("cookies-choosing"),
Step::DownAt("open-export", 20.0, 16.0),
Step::UpAt("open-export", 20.0, 16.0),
Step::Shot("export"),
Step::Key("escape"),
Step::DownAt("open-csv", 20.0, 16.0),
Step::UpAt("open-csv", 20.0, 16.0),
Step::Shot("csv-import"),
Step::Key("escape"),
Step::DownAt("web", 240.0, 100.0),
Step::UpAt("web", 240.0, 100.0),
Step::DownAt("converter", 24.0, 60.0),
@@ -89,6 +98,7 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
.child(codes::render(window, cx))
.child(asking::render(window, cx))
.child(notices::render(window, cx))
.child(transfer::render(window, cx))
.child(web::render(cx))
.into_any_element()
}
+144
View File
@@ -0,0 +1,144 @@
use ely_gpui_component::{
buttons::Button,
misc::{CsvImporter, ExportDialog, ExportFormat, ImportDialog, ImportField},
theme::ActiveTheme,
};
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div};
use crate::probe::probe;
use crate::ui::section;
const PEOPLE: &str = "Name,Email,Team\nAda Lovelace,ada@example.com,Engines\n\"Lin, Wei\",lin@example.com,Maps\nGrace Hopper,grace@example.com,Compilers\n";
/// Which dialog is open.
#[derive(Clone, Copy, PartialEq)]
enum Open {
None,
Export,
Import,
Csv,
}
fn fields() -> [ImportField; 3] {
[
ImportField::new("name").required(),
ImportField::new("email").required(),
ImportField::new("phone"),
]
}
pub fn render(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
let open = window.use_keyed_state("transfer-open", cx, |_, _| Open::None);
let said = window.use_keyed_state("transfer-said", cx, |_, _| SharedString::default());
let shut = |words: String,
open: &gpui::Entity<Open>,
said: &gpui::Entity<SharedString>,
cx: &mut App| {
open.update(cx, |open, cx| {
*open = Open::None;
cx.notify();
});
said.update(cx, |said, cx| {
*said = words.into();
cx.notify();
});
};
let opener = |which: Open, label: &'static str, key: &'static str| {
let open = open.clone();
probe(
key,
div().child(Button::new(key, label).on_click(move |_, _, cx| {
open.update(cx, |open, cx| {
*open = which;
cx.notify();
})
})),
)
};
let (o, s) = (open.clone(), said.clone());
let answer = move |words: String, cx: &mut App| shut(words, &o, &s, cx);
let dialog = match *open.read(cx) {
Open::None => None,
Open::Export => {
let (done, cancel) = (answer.clone(), answer.clone());
Some(
ExportDialog::new(
"export",
"Export the table",
[
ExportFormat::Pdf,
ExportFormat::Png,
ExportFormat::Csv,
ExportFormat::Json,
],
move |format, _, cx| done(format!("Export as .{}", format.extension()), cx),
move |_, cx| cancel("Export cancelled.".into(), cx),
)
.into_any_element(),
)
}
Open::Import => {
let (done, cancel) = (answer.clone(), answer.clone());
Some(
ImportDialog::new(
"import",
"Import people",
["Full name", "E-mail", "Team"],
fields(),
move |mapping, _, cx| done(format!("Columns by field: {mapping:?}"), cx),
move |_, cx| cancel("Import cancelled.".into(), cx),
)
.preview([
vec![
"Ada Lovelace".into(),
"ada@example.com".into(),
"Engines".into(),
],
vec![
"Grace Hopper".into(),
"grace@example.com".into(),
"Compilers".into(),
],
])
.into_any_element(),
)
}
Open::Csv => {
let (done, cancel) = (answer.clone(), answer.clone());
Some(
CsvImporter::new(
"csv",
"Import people.csv",
PEOPLE,
fields(),
move |records, _, cx| done(format!("{} people imported.", records.len()), cx),
move |_, cx| cancel("CSV import cancelled.".into(), cx),
)
.into_any_element(),
)
}
};
div()
.child(
section(
"ExportDialog / ImportDialog / CSVImporter",
"Data out in a format the owner writes, and data in: columns mapped to the app's fields, matched by name to start, and a CSV file read into records through the same mapping.",
cx,
)
.child(
div()
.flex()
.flex_wrap()
.gap_2()
.child(opener(Open::Export, "Export…", "open-export"))
.child(opener(Open::Import, "Import…", "open-import"))
.child(opener(Open::Csv, "Import people.csv…", "open-csv")),
)
.child(
div()
.text_color(cx.theme().colors.fg_muted)
.child(said.read(cx).clone()),
),
)
.children(dialog)
}