+ Learn +
+ ++ Katas +
+katas
+ ++ Have questions? +
+ ++ Still have questions? Talk to support. +
+ +diff --git a/.github/workflows/eleventy-build.yml b/.github/workflows/eleventy-build.yml index 2f5428c9..078ed23a 100644 --- a/.github/workflows/eleventy-build.yml +++ b/.github/workflows/eleventy-build.yml @@ -20,8 +20,7 @@ jobs: - run: npm run build - # New step to copy files from _site to root - - name: Copy files to root + - name: copy files to root run: | cp -R _site/* . rm -rf _site diff --git a/.gitignore b/.gitignore index b70525b5..b512c09d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -_site node_modules \ No newline at end of file diff --git a/_site/LICENSE/index.html b/_site/LICENSE/index.html new file mode 100644 index 00000000..cea83d39 --- /dev/null +++ b/_site/LICENSE/index.html @@ -0,0 +1,17 @@ +
MIT License
+Copyright (c) 2023 Spinal Developers
+Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
diff --git a/_site/README/index.html b/_site/README/index.html new file mode 100644 index 00000000..73e697d4 --- /dev/null +++ b/_site/README/index.html @@ -0,0 +1,46 @@ +An 11ty documentation site theme, built with Tailwind CSS.
+ +git clone https://github.com/spinalcms/11ty-docs-template.git documentation
+
+cd documentation
+
+npm install
+
+npm run dev
+
+npm run build
+
+To change title, description, etc. Update files in src/_data
.
git clone
to your local development machine.git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)+ Learn +
+ +katas
+ ++ Still have questions? Talk to support. +
+ ++ +
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Main +
++ This is the Concepts Homepage +
+ + + + ++ Watch this 5 minutes video-walkthrough of Concepts. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Concept Number 1 +
+ ++ This Concept Number 2 +
+ ++ This is Concept Number 3 +
+ ++ This is Concept Number 4 +
+ +Enter Content Here
+ ++ Still have questions? Talk to support. +
+ +Content here
+ +Content here. You can use markdown.
+ +katas
+ ++ Getting Started +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Handling Errors in Sprucebot +
+ + + + + + + + + + + + +In the context of Sprucebot, effective error handling is not just desirable but essential. It ensures that your application maintains high reliability, excellent user experience, and ease of debugging. This section of the guide will delve deep into strategies for robust error handling in your Sprucebot platform.
++ Guide to Spruce Events and Mercury Event Engine +
+ + + + + + + + + + + + +In Spruce, events are the communicative threads that connect different parts of the system, allowing for a reactive and synchronized ecosystem. This guide is your comprehensive resource for mastering events within Spruce.
+Events in Spruce are triggered by user interactions, system updates, or scheduled occurrences. These events are managed by Mercury, the central nervous system of Spruce, which facilitates communication between skills.
+Mercury processes events through commands like spruce create.event
, spruce listen.event
, and spruce sync.events
, ensuring the smooth operation of your system’s event-driven architecture.
An event is characterized by its fully qualified event name (FQEN), payload, and source. Events are named following a pattern like action-subject::version
to facilitate clear communication and handling.
Core events are foundational and understood by all Spruce components. Examples include:
+create-organization::v2020_01_01
update-role::v2020_01_01
Skill events are custom to specific skills, such as:
+appointments.create-category::v2020_01_10
shifts.create-shift-type::v2020_01_10
Global events are broadcasted across the system and do not target specific recipients.
+Targets are defined using builders, and unless the event is global, a target may look like:
+const acceptEmitTargetBuilder = buildSchema({
+ id: 'acceptEmitTarget',
+ fields: {
+ organizationId: {
+ type: 'id',
+ isRequired: true,
+ },
+ },
+})
+
+export default acceptEmitTargetBuilder
+
+The payload of an event carries the data and is defined based on your requirements. Here’s an example of how to construct a payload:
+import inviteBuilder from '../../../schemas/v2021_12_16/invite.builder'
+
+const sendEmitPayloadBuilder = buildSchema({
+ id: 'sendEmitPayload',
+ fields: {
+ ...dropPrivateFields(
+ dropFields(inviteBuilder.fields, [
+ 'id',
+ 'dateCreated',
+ 'status',
+ 'target',
+ ])
+ ),
+ },
+})
+
+export default sendEmitPayloadBuilder
+
+Emitting events involves invoking specific commands and handling the responses:
+// Emitting an event and handling the response
+const [{ auth }] = await client.emitAndFlattenResponses('whoami::v2020_12_25')
+
+// Listening to events and pushing payloads
+const payloads = []
+const results = await client.emit('test-skill::register-dashboard-cards', {}, ({ payload }) => {
+ payloads.push(payload)
+})
+
+// Handling all payloads and errors
+const { payloads: allPayloads, errors } = eventResponseUtil.getAllPayloadsAndErrors(results, SpruceError)
+assert.isFalsy(errors)
+assert.isEqualDeep(allPayloads, payloads)
+
+Your event emitters are the mechanisms through which your system sends and receives events.
+A local event emitter is a strictly typed, payload-validating event emitter. Here are the steps to create one:
+yarn add @sprucelabs/mercury-event-emitter
+
+import { AbstractEventEmitter } from '@sprucelabs/mercury-event-emitter'
+import { buildEventContract } from '@sprucelabs/mercury-types'
+
+// Defining the contract
+const contract = buildEventContract({
+ eventSignatures: {
+ // Your event signatures here
+ },
+})
+
+// SkillViewEmitter class
+export default class SkillViewEmitter extends AbstractEventEmitter<SkillViewEventContract> {
+ // Your implementation here
+}
+
+// Usage
+const emitter = SkillViewEmitter.getInstance()
+
+const contract = buildEventContract({
+ eventSignatures: {
+ 'did-scroll': {
+ emitPayloadSchema: buildSchema({
+ id: 'didScrollEmitPayload',
+ fields: {
+ scrollTop: {
+ type: 'number',
+ isRequired: true,
+ },
+ },
+ }),
+ },
+ // More event signatures can be added here
+ },
+})
+
+// Attaching a listener
+await instance.on('did-scroll', (payload) => {
+ console.log(payload
+
+.scrollTop)
+})
+
+// Emitting an event
+await emitter.emit('did-scroll', {
+ scrollTop: 0
+})
+
++ Getting Started +
++ This is some intro for this doc. It's styled slightly different. +
+ + + + +Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ This is the Concepts Homepage +
+ + + + ++ Watch this video-walkthrough of Concepts. +
++ Introduction to Views in Sprucebot +
+ ++ Testing in Spruce +
+ ++ Data Stores - The Technical Backbone of Spruce +
+ ++ Spruce Events and Mercury Event Engine +
+ ++ Getting Started +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Implementing Listeners in Spruce +
+ + + + + + + + + + + + +Listeners are important in creating interactive, responsive Skills. They act as the conduits for your application to respond to events happening around it, whether within the system, from user interactions, or external services. This document provides an in-depth guide on implementing listeners effectively in your Skills.
++ This is the Concepts Homepage +
+ + + + + + + + + + + + +Schemas act as a contractual blueprint between data and the way it is handled throughout an application. It’s a critical construct that not only outlines the structure of data entities but also enforces rules and relationships essential for ensuring data integrity, consistency, and reliability.
++ Data Stores - The Technical Backbone of Spruce +
+ + + + + + + + + + + + +Data Stores serve as the pivotal repositories within Spruce’s digital ecosystem. They are fundamentally database-agnostic, providing a uniform interface for data manipulation across various database systems. This technical manual delves into the intricacies of creating, synchronizing, and utilizing Stores to their full potential.
+To start with a Store, use the spruce create.store
command. This initializes a new Store, setting the stage for data management operations.
Post-renaming of classes or files, it’s imperative to align the Store with the changes. This is achieved through the spruce sync.stores
command, ensuring that the Store’s structure and its references remain consistent.
Schemas act as the blueprint for data validation and structure within the Stores. They come in various forms, tailoring to specific actions:
+fullSchema
: Defines the data structure returned by .find(...)
or .findOne(...)
.createSchema
: Ensures data integrity for .create(...)
or .createOne(...)
.updateSchema
: Governs data adjustments made through .update(...)
, .updateOne(...)
, or .upsert(...)
.databaseSchema
: Represents the actual data structure stored in the database.A recommended best practice is to generate a fully typed schema using spruce create.schema
, aligning it with fullSchema
to enforce data consistency.
Hooks are critical in managing the data lifecycle within Stores, allowing for pre- and post-operation data manipulation:
+willCreate
: Invoked before data creation.didCreate
: Triggered after data creation.willUpdate
: Engaged before data updates.didUpdate
: Activated after data updates.willScramble
: Employed to obfuscate data before saving or retrieval for security reasons.These hooks provide a mechanism to fine-tune data before it’s committed to the Store or before it’s fetched from it.
+When integrating Stores within listeners, it’s crucial to manage data fields efficiently. Utilizing includeFields
ensures the Store only retrieves necessary data fields, thus optimizing performance and adherence to response payload constraints.
export default async (
+ event: SpruceEvent<SkillEventContract, EmitPayload>
+): SpruceEventResponse<ResponsePayload> => {
+
+ const { stores, source } = event
+
+ const store = await stores.getStore('profiles')
+ const profile = await store.findOne({
+ target: {
+ personId: source.personId
+ }
+ }, {
+ includeFields: getFields(getProfileSchema),
+ })
+
+ return {
+ profile
+ }
+
+}
+
+Stores also prove their mettle in testing environments. Through seeding and various assertive checks, one can ensure the Store’s performance and reliability. Testing operations like youCanSeedDataIntoYourStore
and helpersLikeGetNewestAndListAreSoNice
provide evidence of a well-functioning Store.
export default class AcceptingAnInviteTest extends AbstractSpruceFixtureTest {
+ private static vc: AcceptSkillViewController
+ private static invites: InvitesStore
+
+ protected static async beforeEach() {
+ await super.beforeEach()
+ this.invites = await this.stores.getStore('invites')
+ }
+
+ @test()
+ @seed('invites', 1)
+ protected static async youCanSeedDataIntoYourStore() {
+ const invite = await this.getNewestInvite()
+ assert.isTruthy(invite)
+ }
+
+ @test()
+ @seed('invites', 20)
+ proctected static async helpersLikeGetNewestAndListAreSoNice() {
+ const invites = this.listInvites()
+ assert.isLength(invites, 20)
+ }
+
+ private static async getNewestInvite() {
+ const invite = await this.invites.findOne({})
+ assert.isTruthy(invite, `Don't forget to @seed('invites', 1) to get started!`)
+ return invite
+ }
+
+ private static async listInvites() {
+ const invites = await this.invites.find({})
+ assert.isAbove(invite.length, 0, `Don't forget to @seed('invites', 1) to get started!`)
+ return invites
+ }
+
+
+}
+
+While Stores are indifferent to the underlying database technology, they can be augmented with adapters like @sprucelabs/postgres-data-store
. This extends the Store’s capabilities to leverage the robustness of SQL-based systems, configured via environment variables like DATABASE_CONNECTION_STRING
. If you want to add Postgres support, you must import the dependency.
yarn add @sprucelabs/postgres-data-store
+
+Then you can configure your databes in your env.
+DATABASE_CONNECTION_STRING="postgres://postgres:password@localhost:5432/database_name"
+
++ Testing in Spruce +
+ + + + + + + + + + + + +In Sprucebot, Tests maintain quality, ensuring that every aspect of the system performs flawlessly. This guide provides insights into the testing methodologies within Sprucebot.
+spruce create.test
- Commits to quality by initializing a new test.
+spruce test
- Validates code readiness for deployment by running the test suite.
Fixtures are utility classes that emulate real-world scenarios, setting up the environment for tests. They are essential for creating realistic test conditions.
+When extending AbstractSpruceFixtureTest
, built-in fixtures are available:
this.views
=> ViewFixturethis.roles
=> RoleFixturethis.locations
=> LocationFixturethis.organizations
=> OrganizationFixturethis.people
=> PersonFixturethis.seeder
=> SeedFixturethis.skills
=> SkillFixturethis.mercury
=> MercuryFixtureexport default class RenderingRootViewControllerTest extends AbstractSpruceFixtureTest {
+
+ @test()
+ protected static gettingFixtures() {
+
+ const organizationFixture = this.organizations
+
+ assert.isTruthy(organizationFixture)
+
+ //Save time by accessing the fixture via protected pro
+ assert.isTruthy(this.organizations)
+ assert.isTruthy(this.locations)
+ }
+}
+
+Authentication mechanisms are tested using the @login
decorator, ensuring security is as robust as other system components.
@login()
+export default class MySkillViewControllerTest extends AbstractSpruceFixtureTest {
+
+ @test()
+ protected static async beforeEach() {
+ await super.beforeEach()
+
+ /**
+ * Is the exact same as @login decorator, don't bother doing this manually
+ * const { client } = await this.Fixture('view').loginAsDemoPerson(DEMO_NUMBER_ROOT_SVC)
+ * MercuryFixture.setDefaultClient(client)
+ **/
+
+ const client = login.getClient()
+ const { client: client2 } = await this.Fixture('view').loginAsDemoPerson()
+
+ assert.isEqual(client, client2) //once default client is set, unless you pass a new number, the client is reused
+
+ const { client: client3 } = await this.Fixture('view').loginAsDemoPerson(DEMO_NUMBER_ROOT_2)
+ assert.isNotEqual(client,client3)
+
+ }
+}
+
+Seeding prepares the testing landscape with necessary data, from roles to profiles, using decorators like @seed
.
//@login sets the default client for all fixtures and seeders going forward
+@login()
+export default class RenderingRootViewControllerTest extends AbstractSpruceFixtureTest {
+
+ @seed('organizations', 2)
+ protected static async beforeEach() {
+ await super.beforeEach()
+
+ const totalOrgs = await this.organizations.listOrganizations()
+ assert.isLength(totalOrgs, 2)
+
+ //since this is in the beforeEach(), every test will come with 2 organizations
+ }
+
+ @test()
+ @seed('locations',10)
+ protected static async locationsShouldSeed() {
+ const currentOrg = await this.organizations.getNewestOrganization()
+ const locations = await this.locations.listLocations({ organizationId: currentOrg?.id })
+
+ assert.isLength(locations, 10)
+ }
+
+ @test()
+ protected static async seedingEntireAccount() {
+
+ // will seed data under newest organization
+ const {
+ locations,
+ guests,
+ managers,
+ owners,
+ teammates
+ } = await this.seeder.seedAccount({
+ totalLocations: 1,
+ totalGuests: 3,
+ totalManagers: 5,
+ totalOwners: 2,
+ totalTeammetes: 3,
+ startingPhone: DEMO_NUMBER_SEED_STARTING_PHONE
+ })
+
+
+ }
+
+}
+
+@login()
+export default class RenderingRootViewControllerTest extends AbstractSpruceFixtureTest {
+
+ @test()
+ @seed('organization',1)
+ @install.skills('skill-namespace-1', 'skill-namespace-2')
+ protected static async skillsArInstalled() {
+ //the skill is only installed at the newest organizatios
+ //now your skill can emit events to skills that are installed at the newest org
+ }
+}
+
+Everything you need to know is under the Views section!
+Creating reusable fixtures, employing meaningful assertions, and anticipating user behavior are among the golden rules of Spruce testing.
+AbstractProfileTest Setup Guide
+To optimize your testing process, start by creating an abstract test class named AbstractProfileTest
. This class will serve as the foundation for all your subsequent tests.
Inheritance: Ensure that every new test class extends the AbstractProfileTest
.
Helper Methods: Implement utility functions such as getNewestInvite()
and listOrgs()
to streamline common actions.
Fixture Initialization:
+beforeEach()
setup method, assign commonly used fixtures to class properties with plural names for easy reference. For example:this.views = this.Fixture('views');
+this.orgs = this.Fixture('organizations');
+
+Store Access:
+beforeEach()
, using the store’s singular name. For instance:this.invites = await this.Store('invites');
+this.profiles = await this.Store('profiles');
+
+Reusability:
+Convenience Getters: For frequently accessed data, create getter methods like getNewestOrganization()
to retrieve information quickly.
Assertions:
+Follow these guidelines to establish a robust and efficient testing structure that will benefit your current and future development needs.
+export default class AbstractProfileTest extends AbstractViewControllerTest {
+ protected static profiles: ProfilesStore
+ protected static router: Router
+
+ protected static async beforeEach() {
+ await super.beforeEach()
+
+ this.profiles = await this.stores.Store('profiles')
+ this.router = this.views.getRouter()
+ }
+
+ protected static async getNewestProfile() {
+ const profile = await this.profiles.findOne({})
+
+ assert.isTruthy(profile, `You gotta @seed('profiles',1) to continue.`)
+ return profile
+ }
+
+ protected static async getNewestOrg() {
+ const org = await this.organizations.getNewestOrganization()
+ assert.isTruthy(org, `You gotta @seed('organizations',1) to continue.`)
+ return org
+ }
+
+ protected static async listProfiles () {
+ const profiles = await this.profiles.findOne({})
+ assert.isAbove(profiles.length, 0, `You gotta @seed('profiles',1) to continue.`)
+ return profiles
+ }
+}
+
++ Introduction to Views in Sprucebot +
+ + + + + + + + + + + + +Skill Views are the elements users interact with when they visit spruce.bot. They are top-level Views that include various components such as CardViews, ListViews, FormViews, etc. These are controlled by SkillViewControllers. Every skill has a RootSkillViewController that is loaded by the skill’s namespace, and there is no limit to the number of Skill Views (and Views) a skill can have.
+To create a new Skill View or View, use the command:
+spruce create.view
+
+For live reloading and previewing of your Views, use:
+spruce watch.views
+
+Note: Your skill must be registered before you can publish your Skill Views.
+This is the primary view for your skill, which is accessible by your Skill’s namespace. The creation of the RootViewController should be the first step for each skill.
+As you make changes to the source, your Views will be incrementally built when you use:
+spruce watch.views
+
+Make sure you have logged in and registered your skill first!
+Testing involves several steps to ensure your Views perform as expected.
+Create Your Test File
+Begin by creating your test file using the command:
spruce create.test
+
+Choose AbstractSpruceFixtureTest
or your Skill’s primary AbstractTest
if it has been created.
Writing Your First Failing Test
+Start by clearing out any existing tests and add your first failing test. Make sure your namespace is correct.
@test()
+protected static async canRenderRootSkillView() {
+ const vc = this.views.Controller('adventure.root', {})
+}
+
+Creating Your Root View Controller
+Use the command spruce create.view
to create your RootViewController.
Finishing Your First Test
+Your test should now ensure that the RootViewController always renders successfully.
@test()
+protected static async canRenderRootSkillView() {
+ const vc = this.views.Controller('adventure.root', {})
+ this.render(vc)
+}
+
+If this test ever fails, it indicates a problem with the RootViewController.
+Several libraries are available to make writing tests easier:
+vcAssert
: A general catch-all library for assertions.formAssert
listAssert
buttonAssert
These are used for constructing failing tests which are an essential part of test-driven development.
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+
+ protected static vc: SpyRootSkillView
+
+ protected static async beforeEach() {
+ await super.beforeEach()
+
+ this.views.setController('adventure.root', SpyRootSkillView)
+ this.vc = this.views.Controller('adventure.root', {}) as SpyRootSkillView
+ }
+
+ @test()
+ protected static async rendersList() {
+ const listVc = listAssert.cardRendersList(this.equipCardVc)
+
+ listAssert.listRendersRow(vc, 'no-entries')
+
+ listVc.addRow({...})
+ listVc.addRow({...})
+ listVc.addRow({ id: location.id, ...})
+
+ await interactor.clickInRow(vc, 2, 'edit')
+ await interactor.clickInRow(vc, location.id, 'edit')
+ }
+
+ protected static get equipCardVc() {
+ return this.vc.equipCardVc
+ }
+
+}
+
+//for accessing the list vc
+class SpyRootSkillView extends RootSkilLViewController {
+ public equipCardVc: CardViewController
+ public equipmentListVc: ListViewController
+}
+
+//production
+class RootSkillviewController extends AbstractSkillViewController {
+ protected equipCardVc: CardViewController
+ protected equipmentListVc: ListViewController
+ public constructor(options: ViewControllerOptions) {
+ super(options)
+ this.equipmentListVc = this.Controller('list', {...})
+ this.equipCardVc = this.Controller('card', ...)
+ }
+}
+Testing confirmation dialogs
+//test
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+ @test()
+ protected static async confirmsBeforeSaving() {
+ const formVc = this.vc.getFormVc();
+
+ formVc.setValue("name", "Haircut");
+
+ const confirmVc = await vcAssert.assertRendersConfirm(this.vc, () =>
+ interactor.submitForm(formVc)
+ );
+
+ await confirmVc.accept();
+
+ const match = await this.Store("services").findOne({});
+
+ assert.isEqual(match.name, "Haircut");
+ }
+
+ @test()
+ protected static async rejectingConfirmDoesNotSave() {
+ const formVc = this.vc.getFormVc();
+
+ await formVc.setValue("name", "Haircut");
+
+ const confirmVc = await vcAssert.assertRendersConfirm(this.vc, () =>
+ interactor.submitForm(formVc)
+ );
+
+ await confirmVc.reject();
+
+ const match = await this.Store("services").findOne({});
+
+ assert.isNotEqual(match.name, "Haircut");
+ }
+}
+
+//production
+class RootSkillviewController extends AbstractSkillViewController {
+ public constructor(options: ViewControllerOptions) {
+ super(options);
+ this.formCardVc = this.FormCardVc();
+ }
+
+ private FormVc() {
+ return this.views.Controller(
+ "form",
+ buildForm({
+ id: "service",
+ schema: serviceSchema,
+ onSubmit: this.handleSubmit.bind(this),
+ sections: [
+ {
+ fields: [
+ {
+ name: "name",
+ hint: "Give it something good!",
+ },
+ "duration",
+ ],
+ },
+ ],
+ })
+ );
+ }
+
+ private FormCardVc() {
+ return this.views.Controller("card", {
+ id: "service",
+ header: {
+ title: "Create your service!",
+ },
+ body: {
+ sections: [
+ {
+ form: this.formVc.render(),
+ },
+ ],
+ },
+ });
+ }
+
+ private async handleSubmit({ values }: SubmitHandler<ServiceSchema>) {
+ const confirm = await this.confirm({ message: "You ready to do this?" });
+ if (confirm) {
+ await this.createService(values);
+ }
+ }
+
+ public getFormVc() {
+ return this.formVc;
+ }
+}
+
+A card with a list that is wicked easy to use and cuts out a ton of reduntant work!
+Make sure you load your Active Record Card for it to show any results!
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+ @test()
+ protected static async rendersActiveRecordCard() {
+ const vc = this.views.Controller("my-skill.root", {});
+ const activeVc = vcAssert.assertSkillViewRendersActiveRecordCard(vc);
+ assert.isEqual(vc.getActiveRecordCard(), activeVc);
+
+ await this.views.load(vc);
+
+ assert.isTrue(activeVc.getIsLoaded());
+ }
+}
+
+// Production
+export default class RootSkillViewController extends AbstractViewController<Card> {
+ public constructor(options: ViewControllerOptions) {
+ super(options);
+ this.activeRecrodCardVc = this.ActiveRecordVc();
+ }
+
+ private ActiveRecordVc() {
+ return this.views.Controller(
+ "activeRecordCard",
+ buildActiveRecordCard({
+ header: {
+ title: "Your locations",
+ },
+ eventName: "list-locations::v2020_12_25",
+ payload: {
+ includePrivateLocations: true,
+ },
+ responseKey: "locations",
+ rowTransformer: (location) => ({ id: location.id, cells: [] }),
+ })
+ );
+ }
+
+ public load(options: SkillViewControllerLoadOptions) {
+ const organization = await options.scope.getCurrentOrganization();
+ this.activeRecordCardVc.setTarget({ organizationId: organization.id });
+ }
+
+ public getActiveRecordCardVc() {
+ return this.activeRecordCardVc;
+ }
+}
+
+Scoping experience to a specific organization or location.
+By default, you will be scoped to your latest organization and location.
+Learn more here.
+@login()
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+ protected static async beforeEach() {
+ await super.beforeEach();
+ }
+
+ @test()
+ protected static async redirectsWhenNoCurrentOrg() {
+ let wasHit = false;
+
+ await vcAssert.assertActionRedirects({
+ router: this.views.getRouter(),
+ action: () => this.views.load(this.vc),
+ destination: {
+ id: "organization.add",
+ },
+ });
+ }
+
+ @test()
+ @seed("organization", 1)
+ protected static async doesNotRedirectWhenCurrentOrg() {
+ const organization = await this.fakedOrganizations[0]
+
+ //this is optional, the current org defaults to the newest added
+ //this.views.getScope().setCurrentOrganization(organization.id)
+
+ await vcAssert.assertActionDidNotRedirect({
+ router: this.views.getRouter(),
+ action: () => this.views.load(this.vc),
+ });
+
+ assert.isEqualDeep(this.vc.currentOrg, organization);
+ }
+
+ @test()
+ @seed("organizations", 3)
+ protected static async usesOrgFromScope() {
+ // since scope loads the newest org by default, we can set
+ // it back to the first org to test our productions code
+ const [organizations] = await this.fakedOrganizations
+
+ this.views.getScope().setCurrentOrganization(organization.id);
+
+ let wasHit = false;
+
+ await this.views.load(this.vc);
+
+ assert.isEqualDeep(this.vc.currentOrg, organization);
+ }
+}
+
+//production
+class RootSkillviewController extends AbstractSkillViewController {
+ public async load(options: SkillViewControllerLoadOptions) {
+ const organization = await options.scope.getCurrentOrganization();
+
+ if (!organization) {
+ await options.router.redirect("organization.add" as any);
+ return;
+ }
+
+ this.currentOrganization = organization;
+ this.profileCardVc.setRouter(options.router);
+ this.profileCardVc.setIsBusy(false);
+ }
+}
+
+//test
+@login()
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+ @test()
+ protected static rendersStats() {
+ vcAssert.assertRendersStats(this.vc.getCardVc());
+ }
+
+ @test()
+ @seed("organization", 1)
+ protected static async rendersExpectedStatsAfterLoad() {
+ await this.bootAndLoad();
+ }
+
+ private static async bootAndLoad() {
+ await this.bootSkill();
+ await this.views.load(this.vc);
+ }
+}
+
+//production
+class RootSkillViewController extends AbstractSkillViewController {
+ public constructor(options: ViewControllerOptions) {
+ super(options);
+
+ this.cardVc = this.CardVc();
+ }
+
+ public async load(options: SkillViewControllerLoadOptions) {}
+}
+
+It is important that you test the graceful handling of failed requests on save. Use the eventMocker to make events throw so you can gracefully handle them!
+//test
+@login()
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+
+ @test()
+ protected static async showsErrorWhenSavingFails() {
+
+ await eventMocker.makeEventThrow('create-organization::v2020_01_01')
+
+ const formVc = this.vc.getFormVc()
+ formVc.setValues({...})
+
+ await vcAssert.assertRenderAlert(this.vc, () => interactor.submitForm(formVc))
+ }
+
+
+ @test()
+ protected static async savesOrgWhenSubmittingForm() {
+ const formVc = this.vc.getFormVc()
+ await formVc.setValues({...})
+ await vcAssert.assertRendersSuccessAlert(this.vc, () => interactor.subimForm(formVc))
+
+ ...
+ }
+}
+
+//production
+class RootSkillviewController extends AbstractSkillViewController {
+ public constructor(options: SkillViewControllerOptions) {
+ super(options)
+ this.formVc = this.FormVc()
+ }
+
+ private FormVc() {
+ return this.views.Controller('form', buildForm({
+ ...,
+ onSubmit: this.handleSubmit.bind(this)
+ }))
+ }
+
+ private async handleSubmit() {
+ const values = this.formVc.getValues()
+
+ try {
+ const client = await this.connectToApi()
+ await client.emitAndFlattenResponses('create-organization::v2020_01_01', {
+ payload: values
+ })
+
+ await this.alert({ message: 'You did it!!', style: 'success' })
+
+ } catch (err: any) {
+ await this.alert({ message: err.message })
+ }
+ }
+}
+
+//test
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+ @test()
+ protected static rendersToolBelt() {
+ tt.assertDoesNotRenderToolBelt(this.vc)
+
+ await this.views.load(this.vc)
+
+ const toolBeltVc = vcAssert.assertRendersToolBelt(this.vc)
+ const toolVc = vcAssert.assertToolBeltRendersTool(this.vc, 'edit')
+
+ assert.isTruthy(toolVc, 'Your ToolBelt does not render a tool with a properly rendered CardVc.')
+
+ }
+
+ @test()
+ protected static async addsTitleSubTitleCard() {
+ // check if a tool is an instance of a specific Class
+ const toolVc = vcAssert.assertToolInstanceOf(
+ this.toolBeltVc,
+ 'title',
+ EventTitleCardViewController
+ )
+
+ // check if tool is accessible off the parent view controller
+ assert.isEqual(toolVc, this.vc.getTitleCardVc())
+ }
+
+}
+
+//production
+class RootSkillviewController extends AbstractSkillViewController {
+ public constructor(options: SkillViewControllerOptions) {
+ super(options)
+
+ this.toolBeltVc = this.ToolBelt()
+ }
+
+ private ToolBelt() {
+ return this.views.Controller('toolBelt', {
+ ...,
+ })
+ }
+
+ public async load(options: SkillViewControllerLoadOptions) {
+ this.toolBeltVc.addTool({
+ id: 'edit',
+ lineIcon: 'globe',
+ card: this.views.Controller('card', { ... })
+ })
+ }
+
+ public renderToolBelt() {
+ return this.toolBeltVc.render()
+ }
+}
+
+//test
+export default class RootSkillViewControllerTest extends AbstractSpruceFixtureTest {
+ @test()
+ protected static redirectsOnSelectLocation() {
+ const locationsCardVc = this.vc.getLocationsCardVc()
+ const location = await this.views.getScope().getCurrentLocation()
+
+ await vcAssert.assertActionRedirects({
+ router: this.views.getRouter(),
+ action: () =>
+ interactor.clickButtonInRow(
+ locationsCardVc.getListVc(),
+ location.id,
+ 'edit'
+ ),
+ destination: {
+ id: 'locations.root',
+ args: {
+ locationId: location.id,
+ },
+ },
+ })
+ }
+}
+
+//production
+class RootSkillviewController extends AbstractSkillViewController {
+ public constructor(options: SkillViewControllerOptions) {
+ super(options)
+ this.locationsCardVc = this.ActiveRecordCardVc()
+ }
+
+ public async load(options: SkillViewControllerLoadOptions) {
+ this.router = options.router
+ await this.locationsCardVc.load()
+ }
+
+ private activeRecordCardVc() {
+ return this.views.Controller('activeRecordCard', buildActiveRecordCard({
+ ...,
+ rowTransformer: (location) => ({
+ id: location.id
+ cells: [
+ {
+ text: {
+ content: location.name
+ },
+ },
+ {
+ button: {
+ id: 'edit',
+ onClick: async () => {
+ await this.router?.redirect('locations.root', {
+ locationId: location.id
+ })
+ }
+ }
+ }
+ ]
+ })
+ }))
+ }
+
+ public getLocationsCardVc() {
+ return this.locationsCardVc
+ }
+}
+
+Using @fake.login()
, you can simulate a client for API requests, which is useful for testing authentication-related scenarios.
To inspect your bundled view controller source, set the VIEW_PROFILER_STATS_DESTINATION_DIR
in your skill’s environment. This will output a file that can be analyzed to see what is included in your bundle.
The document ends with practical hints for testing, such as looking at existing skills, using spruce watch.views
for a real-time feedback loop, and checking out vcAssert.test.ts
in heartwood-view-controllers
for examples.
+ Content Types +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ +
+ ++ Still have questions? Talk to support. +
+ ++ Learn +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Learn +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Learn +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Learn +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ This is the Concepts Homepage +
+ + + + + + ++ Watch this 5 minutes video-walkthrough of Katas. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Kata Number 1 +
+ ++ This Kata Number 2 +
+ ++ This is Kata Number 3 +
+ ++ This is Kata Number 4 +
+ ++ In this section of the guide, you will learn how to build skill views and implement dynamic routing. +
+ + + + + + ++ Watch this 5 minutes video-walkthrough of Katas. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
+spruce .create
The example skill is “Eight-bit stories”.# Replace with the actual command to start a new skill
+
+# Replace with the actual command to install dependencies
+
+# Replace with the actual commands to set up the IDE
+
+ # Replace with the actual code snippet for creating a new test
+
+spruce set.remotes
# Replace with the actual commands to setup remote and install features
+
+# Replace with the actual command to create the view controller
+
+# Replace with the actual code snippet for creating a card component
+
+# Replace with the actual code snippet for beforeEach hook and assertion
+
+
+# Replace with the actual code snippet for dynamic card rendering
+
+# Replace with the actual code snippet for button existence test
+
+# Replace with the actual code snippet for button implementation
+
+# Replace with the actual code snippet for changing visibility
+
+# Replace with the actual code snippet for getter method
+
+# Replace with the actual code snippet for type safety and casting
+
+
+Elaborate
+watch.views
to sync code with tests.spruce set.remotes
# Replace with the actual commands to setup remote and install features
+
+# Replace with the actual command to create the view controller
+
+# Replace with the actual code snippet for creating a card component
+
+# Replace with the actual code snippet for beforeEach hook and assertion
+
+
+# Replace with the actual code snippet for dynamic card rendering
+
+# Replace with the actual code snippet for button existence test
+
+# Replace with the actual code snippet for button implementation
+
+# Replace with the actual code snippet for changing visibility
+
+# Replace with the actual code snippet for getter method
+
+# Replace with the actual code snippet for type safety and casting
+
+
+Elaborate
+watch.views
to sync code with tests.Certainly! Based on the format and content you’ve provided, here is a continuation of the guide:
+Understood. Let’s create an exhaustive guide that mirrors the step-by-step process detailed in the 47-minute video transcript. This guide will include each action, decision point, and explanation provided in the video, offering a user a comprehensive set of instructions that leave no detail out. Code snippet and command placeholders will be clearly marked for you to complete with the specific technical content.
+Title: Building a Root Skill View - Comprehensive Instructional Guide
+Introduction:
+This guide is a thorough instruction manual for building a root skill view. It follows a detailed transcript, ensuring users can execute each step without additional resources.
Docker Configuration for NPM Caching:
+spruce start.cache
+
+Creating the Skill Space:
+(skillname)
with your desired name (not starting with a number).spruce create.skill (skillname)
+
+Preparing the Development Environment:
+cd (skillname) && code .
+
+spruce setup.vscode
+
+Start Test Monitoring (WatchMode):
+spruce test --watchMode smart
+
+Create the First Behavioral Test:
+create.test
+
+[Behavioral test code snippet]
Building the Root Skill View:
+spruce create.view
+
+Implementing the Root Skill View Logic:
+RootSkillViewController
here: [UI elements implementation code snippet]
[UI behavior and error handling code snippet]
Testing the UI Components:
+// Example test case for rendering a card
+it('renders a card', () => {
+ const card = rootSkillViewController.renderCard();
+ assert(card).toBeVisible();
+});
+
+Implementing Dynamic Routing:
+[Navigation routes definition and testing code snippet]
redirectToMetaView
and ensure they work as intended.Previewing on a Device:
+By following this guide, you will execute a complete setup, develop a root skill view using TDD, and ensure your skill works seamlessly across devices. Each placeholder marks where detailed technical input is required, making the guide a comprehensive walkthrough from start to finish.
+ ++ This is the Katas Homepage +
+ + + + + + ++ Watch this 5 minutes video-walkthrough of Katas. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Kata Number 1 +
+ ++ This Kata Number 2 +
+ ++ This is Kata Number 3 +
+ ++ This is Kata Number 4 +
+ +Enter Content Here
+ +Content specific to this subsection.
+ ++ This is the Concepts Homepage +
+ + + + + + ++ Watch this video-walkthrough of Katas. +
++ This Kata Number 1 +
+ ++ This Kata Number 2 +
+ ++ This is Kata Number 3 +
+ ++ This is Kata Number 4 +
+ ++ This is the Kata Homepage +
+ + + + + + ++ Watch this 5 minutes video-walkthrough of Katas. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Kata Number 1 +
+ ++ This Kata Number 2 +
+ ++ This is Kata Number 3 +
+ ++ This is Kata Number 4 +
+ ++ Learn +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Learn +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Learn +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ This is the Concepts Homepage +
+ + + + + + ++ Watch this 5 minutes video-walkthrough of Katas. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Kata Number 1 +
+ ++ This Kata Number 2 +
+ ++ This is Kata Number 3 +
+ ++ This is Kata Number 4 +
+ +Content here
+ +Content here. You can use markdown.
+ +katas
+ +katas
+ +Content here
+ +Content here. You can use markdown.
+ +katas
+ +katas
+ ++ In this section of the guide, you will learn how to manage your metadata. +
+ + + + + + ++ Watch this video-walkthrough of Katas. +
+Welcome to our step-by-step guide on managing metadata! Today, we’re going on an exciting journey to build a dynamic system for handling metadata. This guide will walk you through every step with clear explanations and practical examples. Let’s dive in!
+First things first, let’s create a form to collect metadata. Imagine this form as the gateway through which users will input their data.
+Layout Design:
+(add layout design code here)
+
+Implementing Validation:
+(add validation code here)
+
+Data Binding:
+(add data binding code here)
+
+With our form ready, let’s define how our data should look and behave using schemas.
+Define a Schema:
+(add schema definition code here)
+
+Linking Schema to Form:
+(add integration code here)
+
+Evolving the Schema:
+ (add schema update code here)
+
+Our form and schema are set. Let’s now focus on making them communicate with the backend.
+Setting Up Event Emitters:
+(add event emitter code here)
+
+Processing Events with Listeners:
+(add event listener code here)
+
+We must ensure only the right people have access to the right data.
+Defining User Roles:
+(add roles definition code here)
+
+Implementing Permission Checks:
+(add permission check code here)
+
+Finally, we need to store our data so that it’s not lost when the app closes.
+Choosing a Data Store:
+(add rationale here)
+
+Saving Data:
+(add data saving code here)
+
+Retrieving Data:
+(add data retrieval code here)
+
+Congratulations! You’ve just walked through the process of creating a system to manage metadata. Remember, the journey doesn’t end here. Keep experimenting, keep learning, and most importantly, have fun coding!
+ ++ In this section of the guide, you will learn how to build skill views and implement dynamic routing. +
+ + + + + + ++ Watch this video-walkthrough of Katas. +
+Run the command spruce start.cache
This initializes Docker to use aggressive caching for NPM packages, which improves the speed of dependency retrieval.
+Create a new skill environment by typing spruce create.skill (yourSkillName)
. For example, our test skill is called
“kata-1”:
+spruce create.skill kata-1
+
+You’ll need to provide a name and description for your skill.
+Note: Skill names can’t start with a number.
+Next, the system will install various dependencies.
+To open your skill in Visual Studio Code, type cd (yourSkillName) && code .
. For instance:
cd kata-1 && code .
+
+Inside Visual Studio Code, go to the terminal and run:
+spruce setup.vscode
+
+The terminal will guide you through finalizing the setup for debugging, building, testing, linting, and configuring watchers for starting Visual Studio Code.
+When done, use the command palette to find Manage Automatic Tasks and select Allow Automatic Tasks.
+Reopen the command palette and choose Reload Window.
+This will start tasks that enable testing. The tests will run, spot any missing dependencies, and install them as needed.
+In your external terminal, type:
+spruce test --watchMode smart
+
+This command continuously runs tests you’re developing, and upon success, it starts the remaining tests. It’s recommended to keep this on during development.
+With WatchMode on, go back to Visual Studio Code and the command palette, and run create.test
.
+You’ll choose between two types of testing:
We’ll focus on Behavioural tests due to the significant changes we’re implementing, especially when integrating with other services.
+Select Behavioural tests, and you’ll be asked to specify what you’re testing, using camel case.
+# Enter the actual commands for the IDE setup here
+
+Write a new behavioral test called “RootSkillView”.
+Base the test on the “abstract spruce fixture test”.
# Enter the actual code snippet for creating a new test here
+
+After setting up the IDE and starting testing, the next step is to build the skill interfaces, starting with the root skill interface.
+Use the command spruce create.view
to begin making your root skill interface.
spruce create.view
+
+When asked, choose Skill View Controller
for your controller type.
For the controller name, type root
.
The command-line will handle file generation and setup for your root skill interface.
+display the code structure here
In your RootSkillViewController
, add basic UI elements like cards, buttons, and images.
enter code snippet for UI elements here
Describe the elements and how they interact, such as what happens when buttons are clicked.
+enter code snippet for UI interactions and error handling here
// Example of button implementation
+handleButtonClick() {
+ // Response logic for button click
+}
+
+Include error management and plan for when users deviate from the expected interaction path.
+Write a test to make sure the root skill interface is rendered correctly.
+Use the assert
library to check for the existence and proper display of all UI components.
// Testing card display example
+it('should show a card', () => {
+ const card = rootSkillViewController.renderCard();
+ assert(card).toBeVisible();
+});
+
+Run your tests using spruce test
in watch mode to continuously monitor for changes and mistakes.
Define routes in your skill interface to allow for smooth transitions between different views.
+enter snippet for route definition and testing here
Create functions to manage the routes, like redirectToMetaView
or returnToRootView
.
// Example of a redirect function
+
+Design tests to ensure your routing works correctly, particularly in handling user actions.
+Test the skill interface on an actual device to ensure the UI and navigation feel natural and responsive.
+If possible, use emulation tools in your development environment to simulate different devices.
+Adjust the UI based on device preview feedback.
+ ++ In this section of the guide, you will learn how to manage your metadata. +
+ + + + + + ++ Watch this video-walkthrough of Katas. +
++ Developer Onboarding +
++ This is the Katas Homepage +
+ + + + + + ++ Watch this 5 minutes video-walkthrough of Katas. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Kata Number 1 +
+ ++ This Kata Number 2 +
+ ++ This is Kata Number 3 +
+ ++ This is Kata Number 4 +
+ +Enter Content Here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Intro +
++ This is the Katas Homepage +
+ + + + ++ Watch this 5 minutes video-walkthrough of Concepts. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Concept Number 1 +
+ ++ This Concept Number 2 +
+ ++ This is Concept Number 3 +
+ ++ This is Concept Number 4 +
+ +Enter Content Here
+ ++ Still have questions? Talk to support. +
+ ++ Developer Onboarding +
++ This is the Katas Homepage +
+ + + + + + + + + +Enter Content Here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Intro +
++ This is the Katas Homepage +
+ + + + + + ++ Watch this 5 minutes video-walkthrough of Katas. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Kata Number 1 +
+ ++ This Kata Number 2 +
+ ++ This is Kata Number 3 +
+ ++ This is Kata Number 4 +
+ +Enter Content Here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Main +
++ This is the Concepts Homepage +
+ + + + ++ Watch this 5 minutes video-walkthrough of Concepts. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Concept Number 1 +
+ ++ This Concept Number 2 +
+ ++ This is Concept Number 3 +
+ ++ This is Concept Number 4 +
+ +Enter Content Here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Katas +
+katas
+ ++ Still have questions? Talk to support. +
+ ++ Intro +
++ This is the Katas Homepage +
+ + + + ++ Watch this 5 minutes video-walkthrough of Concepts. You quickly learn how to set up your dashboard, invite team members, set permissions and how to schedule and publish content. +
++ This Concept Number 1 +
+ ++ This Concept Number 2 +
+ ++ This is Concept Number 3 +
+ ++ This is Concept Number 4 +
+ +Enter Content Here
+ ++ Still have questions? Talk to support. +
+ ++ dev +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ dev +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Getting Started +
++ This is some intro for this doc. It's styled slightly different. +
+ + + + + +Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
++ This is some intro for this doc. It's styled slightly different. +
+ + + + + +Content here. You can use markdown.
+ ++ Still have questions? Talk to support. +
+ ++ Concepts +
+katas
+ ++ Still have questions? Talk to support. +
+ +Content here
+ +Content here. You can use markdown.
+ +katas
+ ++ This is the Ideology Homepage +
+ + + + + ++ Watch this video-walkthrough of Ideology. +
++ This Ideology Number 1 +
+ ++ This Ideology Number 2 +
+ ++ This is Ideology Number 3 +
+ ++ This is Ideology Number 4 +
+ +Enter Content Here
+ ++ ideology +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Content +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Content Types +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Content +
+Content here
+ ++ Still have questions? Talk to support. +
+ ++ Welcome to the developer portal of the Lumena Platform, anchored by Sprucebot. This guide is designed to be a self-serve tool that will enable both individuals and teams to use, develop, and expand on the Lumena Platform. +
+ + + + + + ++ Delve deeper into the foundational principles that drive Sprucebot. Gain understanding of the philosophy, core belief, and the vision that underpins Sprucebot. +
+ ++ Gain a solid understanding of the fundamental concepts around Sprucebot. This section offers a deep dive into Sprucebot architecture, functionalities, and key features. +
+ ++ Whether you're a seasoned developer or just starting out, our onboarding materials ensure a seamless initiation process into the Sprucebot ecosystem. +
+ ++ Resources, tutorials, and articles that will further empower your development journey with Sprucebot. +
+ ++ Talk to Support. +
+ + + + +