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

feat: Add react template #295

Merged
merged 56 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
820daf8
WIP
c12i May 22, 2024
a595f95
update ui module
c12i May 24, 2024
f6202a2
add react to matched ui framework strings
c12i May 24, 2024
1d2474c
hbsify templates
c12i May 27, 2024
fd3b0ef
add base react components
c12i May 27, 2024
7bdaa86
react collection
c12i May 27, 2024
c13058f
react detail component
c12i May 27, 2024
1007ddc
react create component
c12i May 28, 2024
50e65d8
react edit template
c12i May 28, 2024
aa91ac3
react entry type for linked from component
c12i May 28, 2024
ccd75c5
field types
c12i May 28, 2024
a2e513f
react link type components
c12i May 28, 2024
6d0f2fa
fix field types
c12i May 28, 2024
0fe3eeb
template swoop; fix issues
c12i May 29, 2024
610396c
add missing imports, strip out eslint
c12i May 29, 2024
7bf8a4a
fix images
c12i May 29, 2024
27d03dc
refactor holochain provider
c12i May 29, 2024
b6b88ea
fix entry-type templates
c12i May 29, 2024
58147cf
fix link-type templates
c12i May 29, 2024
066affa
correctly name field type widgets
c12i May 29, 2024
333f0f1
add forum example
c12i May 29, 2024
92686fb
fix holochain context
c12i May 29, 2024
a261781
check for links lengths before setting state
c12i May 29, 2024
844b9df
fix text field
c12i May 29, 2024
db5ffc2
styling improvments
c12i May 29, 2024
0a6d907
extend base styles
c12i May 29, 2024
75506dc
improve example styling
c12i May 29, 2024
7a1d91a
fix mismatched components
c12i May 30, 2024
8ed5a6b
remove ui readme
c12i May 30, 2024
b2cd4af
fix templates
c12i May 31, 2024
65e73b5
fix imports
c12i May 31, 2024
9b6e5e6
update elements and imports
c12i Jun 3, 2024
46c3238
type app signal callback
c12i Jun 3, 2024
bb6f7d4
test with react template in ci
c12i Jun 3, 2024
d0a361f
update styling
c12i Jun 3, 2024
73e3f15
fix global styles
c12i Jun 3, 2024
6f29f68
rename and move client context
c12i Jun 6, 2024
f3ed2fe
make client context compatible with holo
c12i Jun 6, 2024
97532e8
fix indentation in templates
c12i Jun 6, 2024
f86a258
update client context template
c12i Jun 6, 2024
e1eb064
fix entry-types for linked from component props
c12i Jun 6, 2024
6070204
update detail components
c12i Jun 6, 2024
88306b0
add default field types
c12i Jun 6, 2024
88fd712
fix example app.tsx
c12i Jun 6, 2024
04044db
fix vec detail render
c12i Jun 6, 2024
9fd4979
fix vec edit render
c12i Jun 6, 2024
18751b5
fix timestamp
c12i Jun 10, 2024
da8b781
fix client context not updating loading state
c12i Jun 10, 2024
873d832
fix vec input
c12i Jun 10, 2024
470bdc6
fix vec input setters in edit components
c12i Jun 10, 2024
d37bf7b
Update templates/react/field-types/u32/NumberInput/edit/render.hbs
c12i Jun 11, 2024
47cb363
Update templates/react/field-types/Timestamp/DateTimePicker/edit/rend…
c12i Jun 11, 2024
077205b
Update templates/react/field-types/Timestamp/DateTimePicker/edit/rend…
c12i Jun 11, 2024
7f80f0d
set default and initial values for boolean inputs to false
c12i Jun 11, 2024
519e861
ensure previous zip file is deleted
c12i Jun 11, 2024
20d97c0
fix templates
c12i Jun 11, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
template: [ svelte, vue, lit, vanilla ]
template: [ svelte, vue, lit, react, vanilla ]
steps:
- uses: actions/checkout@v4

Expand Down
10 changes: 3 additions & 7 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use structopt::StructOpt;
pub struct HcScaffold {
#[structopt(short, long)]
/// The template to use for the scaffold command
/// Can either be an option from the built-in templates: "vanilla", "vue", "lit", "svelte", "headless"
/// Can either be an option from the built-in templates: "vanilla", "vue", "lit", "svelte", "react", "headless"
/// Or a path to a custom template
template: Option<String>,

Expand Down Expand Up @@ -233,7 +233,7 @@ impl HcScaffold {
// get the template file tree and the ui framework name or custom template path
let (template, template_file_tree) = match template {
Some(template) => match template.to_lowercase().as_str() {
"lit" | "svelte" | "vanilla" | "vue" | "headless" => {
"lit" | "svelte" | "vanilla" | "vue" | "react" | "headless" => {
let ui_framework = UiFramework::from_str(template)?;
(ui_framework.name(), ui_framework.template_filetree()?)
}
Expand Down Expand Up @@ -938,11 +938,7 @@ impl HcScaffoldTemplate {

match self {
HcScaffoldTemplate::Clone { .. } => {
println!(
r#"Template initialized to folder {:?}
"#,
target_template
);
println!(r#"Template initialized to folder {:?} "#, target_template);
}
}
Ok(())
Expand Down
16 changes: 15 additions & 1 deletion src/scaffold/web_app/uis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ static LIT_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates
static SVELTE_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/svelte");
static VUE_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/vue");
static VANILLA_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/vanilla");
static REACT_TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/react");
static HEADLESS_TEMPLATE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates/headless");

#[derive(Debug, Clone)]
Expand All @@ -20,6 +21,7 @@ pub enum UiFramework {
Lit,
Svelte,
Vue,
React,
Headless,
}

Expand All @@ -31,6 +33,7 @@ impl UiFramework {
UiFramework::Lit => "lit",
UiFramework::Svelte => "svelte",
UiFramework::Vue => "vue",
UiFramework::React => "react",
UiFramework::Headless => "headless",
};
name.to_string()
Expand All @@ -42,6 +45,7 @@ impl UiFramework {
UiFramework::Vanilla => &VANILLA_TEMPLATES,
UiFramework::Svelte => &SVELTE_TEMPLATES,
UiFramework::Vue => &VUE_TEMPLATES,
UiFramework::React => &REACT_TEMPLATES,
UiFramework::Headless => &HEADLESS_TEMPLATE,
};
dir_to_file_tree(dir)
Expand All @@ -52,6 +56,7 @@ impl UiFramework {
UiFramework::Lit,
UiFramework::Svelte,
UiFramework::Vue,
UiFramework::React,
UiFramework::Vanilla,
UiFramework::Headless,
];
Expand All @@ -64,7 +69,12 @@ impl UiFramework {
}

pub fn choose_non_vanilla() -> ScaffoldResult<UiFramework> {
let frameworks = [UiFramework::Lit, UiFramework::Svelte, UiFramework::Vue];
let frameworks = [
UiFramework::Lit,
UiFramework::Svelte,
UiFramework::React,
UiFramework::Vue,
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose UI framework: (Use arrow-keys. Return to submit)")
.default(0)
Expand Down Expand Up @@ -98,6 +108,8 @@ impl TryFrom<&FileTree> for UiFramework {
return Ok(UiFramework::Svelte);
} else if ui_package_json.contains("vue") {
return Ok(UiFramework::Vue);
} else if ui_package_json.contains("react") {
return Ok(UiFramework::React);
} else if !dir_exists(app_file_tree, &PathBuf::from("ui/src")) {
return Ok(UiFramework::Vanilla);
}
Expand All @@ -112,6 +124,7 @@ impl std::fmt::Display for UiFramework {
UiFramework::Vanilla => "vanilla".yellow(),
UiFramework::Lit => "lit".bright_blue(),
UiFramework::Svelte => "svelte".bright_red(),
UiFramework::React => "react".cyan(),
UiFramework::Vue => "vue".green(),
UiFramework::Headless => "headless (no ui)".italic(),
};
Expand All @@ -127,6 +140,7 @@ impl FromStr for UiFramework {
"vanilla" => Ok(UiFramework::Vanilla),
"svelte" => Ok(UiFramework::Svelte),
"vue" => Ok(UiFramework::Vue),
"react" => Ok(UiFramework::React),
"lit" => Ok(UiFramework::Lit),
"headless" => Ok(UiFramework::Headless),
value => Err(ScaffoldError::MalformedTemplate(format!(
Expand Down
4 changes: 4 additions & 0 deletions templates/react/collection.instructions.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{#if (eq collection_type.type "Global")}}
At first, the UI for this application is empty. If you want the newly scaffolded collection to be the entry point for its UI, import the newly
generated <{{pascal_case collection_name}} /> component
{{/if}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { assert, test } from "vitest";

import { runScenario, dhtSync, CallableCell } from '@holochain/tryorama';
import {
NewEntryAction,
ActionHash,
Record,
Link,
AppBundleSource,
fakeActionHash,
fakeAgentPubKey,
fakeEntryHash
} from '@holochain/client';
import { decode } from '@msgpack/msgpack';

import { create{{pascal_case referenceable.name}} } from './common.js';

test('create a {{pascal_case referenceable.name}} and get {{lower_case collection_name}}', async () => {
await runScenario(async scenario => {
// Construct proper paths for your app.
// This assumes app bundle created by the `hc app pack` command.
const testAppPath = process.cwd() + '/../workdir/{{app_name}}.happ';

// Set up the app to be installed
const appSource = { appBundleSource: { path: testAppPath } };

// Add 2 players with the test app to the Scenario. The returned players
// can be destructured.
const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]);

// Shortcut peer discovery through gossip and register all agents in every
// conductor of the scenario.
await scenario.shareAllAgents();

// Bob gets {{lower_case collection_name}}
let collectionOutput: Link[] = await bob.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "get_{{snake_case collection_name}}",
payload: {{#if (eq collection_type.type "Global")}}null{{else}}alice.agentPubKey{{/if}}
});
assert.equal(collectionOutput.length, 0);

// Alice creates a {{pascal_case referenceable.name}}
const createRecord: Record = await create{{pascal_case referenceable.name}}(alice.cells[0]);
assert.ok(createRecord);

await dhtSync([alice, bob], alice.cells[0].cell_id[0]);

// Bob gets {{lower_case collection_name}} again
collectionOutput = await bob.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "get_{{snake_case collection_name}}",
payload: {{#if (eq collection_type.type "Global")}}null{{else}}alice.agentPubKey{{/if}}
});
assert.equal(collectionOutput.length, 1);
assert.deepEqual({{#if (eq referenceable.hash_type "EntryHash")}}(createRecord.signed_action.hashed.content as NewEntryAction).entry_hash{{else}}createRecord.signed_action.hashed.hash{{/if}}, collectionOutput[0].target);
{{#if (and deletable (eq referenceable.hash_type "ActionHash"))}}

// Alice deletes the {{pascal_case referenceable.name}}
await alice.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "delete_{{snake_case referenceable.name}}",
payload: createRecord.signed_action.hashed.hash
});

await dhtSync([alice, bob], alice.cells[0].cell_id[0]);

// Bob gets {{lower_case collection_name}} again
collectionOutput = await bob.cells[0].callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "get_{{snake_case collection_name}}",
payload: {{#if (eq collection_type.type "Global")}}null{{else}}alice.agentPubKey{{/if}}
});
assert.equal(collectionOutput.length, 0);
{{/if}}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Link, AppSignal, HolochainError{{#if (eq collection_type.type "ByAuthor")}}, AgentPubKey{{/if}}{{#if (eq referenceable.hash_type "EntryHash")}}, NewEntryAction{{/if}} } from '@holochain/client';
import { FC, useCallback, useState, useEffect, useContext } from 'react';

import type { {{pascal_case coordinator_zome_manifest.name}}Signal } from './types';
import {{pascal_case referenceable.name}}Detail from './{{pascal_case referenceable.name}}Detail';
import { ClientContext } from '../../ClientContext';

const {{pascal_case collection_name}}: FC{{#if (eq collection_type.type "ByAuthor")}}<{{pascal_case collection_name}}Props>{{/if}} = ({{#if (eq collection_type.type "ByAuthor")}}{author}{{/if}}) => {
const {client} = useContext(ClientContext);
const [hashes, setHashes] = useState<Uint8Array[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<HolochainError | undefined>();

const fetch{{pascal_case (plural referenceable.name)}} = useCallback(async () => {
setLoading(true)
try {
const links: Link[] = await client?.callZome({
cap_secret: null,
role_name: '{{dna_role_name}}',
zome_name: '{{snake_case coordinator_zome_manifest.name}}',
fn_name: 'get_{{snake_case collection_name}}',
payload: {{#if (eq collection_type.type "ByAuthor")}}author{{else}}null{{/if}},
});
if (links?.length) {
setHashes(links.map((l) => l.target));
}
} catch (e) {
setError(e as HolochainError);
} finally {
setLoading(false);
}
}, [client{{#if (eq collection_type.type "ByAuthor")}}, author{{/if}}]);

const handleSignal = useCallback((signal: AppSignal) => {
if (signal.zome_name !== '{{coordinator_zome_manifest.name}}') return;
const payload = signal.payload as {{pascal_case coordinator_zome_manifest.name}}Signal;
if (payload.type !== 'EntryCreated') return;
if (payload.app_entry.type !== '{{pascal_case referenceable.name}}') return;
{{#if (eq collection_type.type "ByAuthor")}}
if (author.toString() !== client?.myPubKey.toString()) return;
{{/if}}
setHashes((prevHashes) => [...prevHashes, {{#if (eq referenceable.hash_type "ActionHash")}}payload.action.hashed.hash{{else}}(payload.action.hashed.content as NewEntryAction).entry_hash{{/if}}]);
}, [setHashes]);

useEffect(() => {
{{#if (eq collection_type.type "ByAuthor")}}
if (author === undefined) {
throw new Error(`The author prop is required for the {{pascal_case collection_name}} element`);
}
{{/if}}
fetch{{pascal_case (plural referenceable.name)}}();
client?.on('signal', handleSignal);
}, [client, handleSignal, fetch{{pascal_case (plural referenceable.name)}}{{#if (eq collection_type.type "ByAuthor")}}, author{{/if}}]);

if (loading) {
return <progress />;
}

return (
<div>
{error ? (
<span>Error fetching the {{lower_case (plural referenceable.name)}}: {error.message}</span>
) : hashes.length > 0 ? (
<div>
{hashes.map((hash, i) => (
<{{pascal_case referenceable.name}}Detail key={i} {{camel_case referenceable.name}}Hash={hash} on{{pascal_case referenceable.name}}Deleted={fetch{{pascal_case (plural referenceable.name)}}} />
))}
</div>
) : (
<article>No {{lower_case (plural referenceable.name)}} found{{#if (eq collection_type.type "ByAuthor")}} for this author{{/if}}.</article>
)}
</div>
);
};

{{#if (eq collection_type.type "ByAuthor")}}
interface {{pascal_case collection_name}}Props {
author: AgentPubKey
}
{{/if}}

export default {{pascal_case collection_name}};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { CallableCell } from '@holochain/tryorama';
import { NewEntryAction, ActionHash, Record, AppBundleSource, fakeActionHash, fakeAgentPubKey, fakeEntryHash, fakeDnaHash } from '@holochain/client';

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {
Record,
ActionHash,
DnaHash,
SignedActionHashed,
EntryHash,
AgentPubKey,
Create,
Update,
Delete,
CreateLink,
DeleteLink
} from '@holochain/client';

export type {{pascal_case zome_manifest.name}}Signal = {
type: 'EntryCreated';
action: SignedActionHashed<Create>;
app_entry: EntryTypes;
} | {
type: 'EntryUpdated';
action: SignedActionHashed<Update>;
app_entry: EntryTypes;
original_app_entry: EntryTypes;
} | {
type: 'EntryDeleted';
action: SignedActionHashed<Delete>;
original_app_entry: EntryTypes;
} | {
type: 'LinkCreated';
action: SignedActionHashed<CreateLink>;
link_type: string;
} | {
type: 'LinkDeleted';
action: SignedActionHashed<DeleteLink>;
link_type: string;
};

export type EntryTypes = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{{previous_file_content}}

export async function sample{{pascal_case entry_type.name}}(cell: CallableCell, partial{{pascal_case entry_type.name}} = {}) {
return {
...{
{{#each entry_type.fields}}
{{#if linked_from}}
{{#if (ne linked_from.hash_type "AgentPubKey")}}
{{#if (eq cardinality "vector")}}
{{#if (eq (pascal_case linked_from.name) (pascal_case ../entry_type.name))}}
{{field_name}}: [],
{{else}}
{{#if (eq linked_from.hash_type "ActionHash")}}
{{field_name}}: [(await create{{pascal_case linked_from.name}}(cell)).signed_action.hashed.hash],
{{else}}
{{field_name}}: [((await create{{pascal_case linked_from.name}}(cell)).signed_action.hashed.content as NewEntryAction).entry_hash],
{{/if}}
{{/if}}
{{else}}
{{#if (eq (pascal_case linked_from.name) (pascal_case ../entry_type.name))}}
{{field_name}}: null,
{{else}}
{{#if (eq linked_from.hash_type "ActionHash")}}
{{field_name}}: (await create{{pascal_case linked_from.name}}(cell)).signed_action.hashed.hash,
{{else}}
{{field_name}}: ((await create{{pascal_case linked_from.name}}(cell)).signed_action.hashed.content as NewEntryAction).entry_hash,
{{/if}}
{{/if}}
{{/if}}
{{else}}
{{field_name}}: cell.cell_id[1],
{{/if}}
{{else}}
{{#if (eq cardinality "vector")}}
{{field_name}}: [{{> (concat field_type.type "/sample") field_type=field_type}}],
{{else}}
{{field_name}}: {{> (concat field_type.type "/sample") field_type=field_type}},
{{/if}}
{{/if}}
{{/each}}
},
...partial{{pascal_case entry_type.name}}
};
}

export async function create{{pascal_case entry_type.name}}(cell: CallableCell, {{camel_case entry_type.name}} = undefined): Promise<Record> {
return cell.callZome({
zome_name: "{{coordinator_zome_manifest.name}}",
fn_name: "create_{{snake_case entry_type.name}}",
payload: {{camel_case entry_type.name}} || await sample{{pascal_case entry_type.name}}(cell),
});
}

Loading
Loading