gnolang / hackerspace

Tinker, build, explore Gno - without the monorepo!
11 stars 8 forks source link

Zentasktic Builder Journey #64

Open michelleellen opened 7 months ago

michelleellen commented 7 months ago

Hey Dragos,

For your work on the Zentasktic grant, please document your journey here. If you can start by introducing the project and regular updates.

irreverentsimplicity commented 7 months ago

Zentasktic grant consists of 3 back-end projects, with 6 UIs, 2 for each backend, one web, one mobile. As of April 10th, the first backend app, zentaskitc core is implemented here.

ZenTasktic Core

A basic, minimalisitc Asess-Decide-Do implementations as p/zentasktic.

This implementation will expose all the basic features of the framework: tasks & projects with complete workflows. Ideally, this should offer all the necessary building blocks for any other custom implementation.

Object Definitions and Default Values

As an unopinionated ADD workflow, zentastic_core defines the following objects:

Realms act like containers for tasks & projects during their journey from Assess to Do, via Decide. Each realm has a certain restrictions, e.g. a task's Body can only be edited in Assess, a Context, Due date and Alert can only be added in Decide, etc.

If someone observes different realms, there is support for adding and removing arbitrary Realms.

note: the Ids between 1 and 4 are reserved for: 1-Assess, 2-Decide, 3-Do, 4-Collection. Trying to add or remove such a Realm will raise an error.

Realm data definition:

type Realm struct {
    Id          string `json:"realmId"`
    Name        string `json:"realmName"`
}

A task is the minimal data structure in ZenTasktic, with the following definition:

type Task struct {
    Id          string `json:"taskId"`
    ProjectId   string `json:"taskProjectId"`
    ContextId   string `json:"taskContextId"`
    RealmId     string `json:"taskRealmId"`
    Body        string `json:"taskBody"`
    Due         string `json:"taskDue"`
    Alert       string `json:"taskAlert"`
}

Projects are unopinionated collections of Tasks. A Task in a Project can be in any Realm, but the restrictions are propagated upwards to the Project: e.g. if a Task is marked as 'done' in the Do realm (namely changing its RealmId property to "1", Assess, or "4" Collection), and the rest of the tasks are not, the Project cannot be moved back to Decide or Asses, all Tasks must have consisted RealmId properties.

A Task can be arbitrarily added to, removed from and moved to another Project.

Project data definition:

type Project struct {
    Id          string `json:"projectId"`
    ContextId   string `json:"projectContextId"`
    RealmId     string `json:"projectRealmId"`
    Tasks       []Task `json:"projectTasks"`
    Body        string `json:"projectBody"`
    Due         string `json:"ProjectDue"`
}

Contexts act as tags, grouping together Tasks and Project, e.g. "Backend", "Frontend", "Marketing". Contexts have no defaults and can be added or removed arbitrarily.

Context data definition:

type Context struct {
    Id          string `json:"contextId"`
    Name        string `json:"contextName"`
}

Collections are intended as an agnostic storage for Tasks & Projects which are either not ready to be Assessed, or they have been already marked as done, and, for whatever reason, they need to be kept in the system. There is a special Realm Id for Collections, "4", although technically they are not part of the Assess-Decide-Do workflow.

Collection data definition:

type Collection struct {
    Id          string `json:"collectionId"`
    RealmId     string `json:"collectionRealmId"`
    Name        string `json:"collectionName"`
    Tasks       []Task `json:"collectionTasks"`
    Projects    []Project `json:"collectionProjects"`
}

ObjectPaths are minimalistic representations of the journey taken by a Task or a Project in the Assess-Decide-Do workflow. By recording their movement between various Realms, one can extract their ZenStatus, e.g., if a Task has been moved many times between Assess and Decide, never making it to Do, we can infer the following: -- either the Assess part was incomplete -- the resources needed for that Task are not yet ready

ObjectPath data definition:

type ObjectPath struct {
    ObjectType  string `json:"objectType"` // Task, Project
    Id          string `json:"id"` // this is the Id of the object moved, Task, Project
    RealmId     string `json:"realmId"`
}

note: the core implementation offers the basic adding and retrieving functionality, but it's up to the client realm using the zentasktic package to call them when an object is moved from one Realm to another.

Example Workflow

package example_zentasktic

import "gno.land/p/demo/zentasktic"

// initializing a task, assuming we get the value POSTed by some call to the current realm

newTask := zentasktic.Task{Id: "20", Body: "Buy milk"}
newTask.AddTask()

// if we want to keep track of the object zen status, we update the object path
taskPath := zentasktic.ObjectPath{ObjectType: "task", Id: "20", RealmId: "1"}
taskPath.AddPath()
...

editedTask := zentasktic.Task{Id: "20", Body: "Buy fresh milk"}
editedTask.EditTask()

...

// moving it to Decide

editedTask.MoveTaskToRealm("2")

// adding context, due date and alert, assuming they're received from other calls

shoppingContext := zentasktic.GetContextById("2")

cerr := shoppingContext.AddContextToTask(editedTask)

derr := editedTask.SetTaskDueDate("2024-04-10")
now := time.Now() // replace with the actual time of the alert
alertTime := now.Format("2006-01-02 15:04:05")
aerr := editedTask.zentasktic.SetAlert(alertTime)

...

// move the Task to Do

editedTask.MoveTaskToRealm("2")

// if we want to keep track of the object zen status, we update the object path
taskPath := zentasktic.ObjectPath{ObjectType: "task", Id: "20", RealmId: "2"}
taskPath.AddPath()

// after the task is done, we sent it back to Assess

editedTask.MoveTaskToRealm("1")

// if we want to keep track of the object zen status, we update the object path
taskPath := zentasktic.ObjectPath{ObjectType: "task", Id: "20", RealmId: "1"}
taskPath.AddPath()

// from here, we can add it to a collection

myCollection := zentasktic.GetCollectionById("1")

myCollection.AddTaskToCollection(editedTask)

// if we want to keep track of the object zen status, we update the object path
taskPath := zentasktic.ObjectPath{ObjectType: "task", Id: "20", RealmId: "4"}
taskPath.AddPath()

All tests are in the *_test.gno files, e.g. tasks_test.gno, projects_test.gno, etc.

irreverentsimplicity commented 2 months ago

ZenTasktic package

During implementation, it became obvious that we need to move storage outside the package (packages deployed under p/ cannot store state) and into the realm calling the package. We wanted to maintain the logic separated, though, to follow the ADD workflow. We refactored by creating a manager for each data type (Tasks, Projects, Realms, etc) which returns an avl.Tree that is responsible with the storage in the calling realm.

package zentasktic

import (
    "time"

    "gno.land/p/demo/avl"
)

type Task struct {
    Id          string `json:"taskId"`
    ProjectId   string `json:"taskProjectId"`
    ContextId   string `json:"taskContextId"`
    RealmId     string `json:"taskRealmId"`
    Body        string `json:"taskBody"`
    Due         string `json:"taskDue"`
    Alert       string `json:"taskAlert"`
}

type ZTaskManager struct {
    Tasks *avl.Tree
}

func NewZTaskManager() *ZTaskManager {
    return &ZTaskManager{
        Tasks: avl.NewTree(),
    }
}

// actions

func (ztm *ZTaskManager) AddTask(t Task) error {
    if ztm.Tasks.Size() != 0 {
        _, exist := ztm.Tasks.Get(t.Id)
        if exist {
            return ErrTaskIdAlreadyExists
        }
    }
    ztm.Tasks.Set(t.Id, t)
    return nil
}

ZenTasktic Gno realms

The 3 realms deployed are implemeting the same type of workflow, but with different data types.

The basic one, ZenTasktic Core, has just the basic data types defined in the ZenTasktic package.

The ZenTasktic Project realm is multi-user (has Users which can be allocated to Teams) and Rewards.

The ZenTasktic User realm is a barebone realm suitable for an individual tracking his work on potentially more customers.

ZenTasktic Project

This realm has 3 more data types: Users (Actors), Teams and Points. These are hinting towards a type of task / project which can be worked on, individually, or in a team, for a certain reward. We added a Workable interface, which allows, for instance, to implement specific Tasks as WorkableTasks, and new data types, like WorkDurations (can be expressed in hours, work hours, etc).

Here is a snippet from the current implementation:

type Workable interface {
    // restrict implementation of Workable to this realm
    assertWorkable()
}

type isWorkable struct {}

type WorkableTask struct {
    zentasktic.Task
}

func (wt *WorkableTask) assertWorkable() {}

type WorkableProject struct {
    zentasktic.Project
}

func (wp *WorkableProject) assertWorkable() {}

var _ Workable = &WorkableTask{}
var _ Workable = &WorkableProject{}

type WorkDuration struct {
    Id              string `json:"workDurationId"`
    ObjectId        string `json:"objectId"`
    ObjectType      string `json:"objectType"` // Task, Project
    Duration        time.Duration `json:"workDuration"`
    Workable        Workable      `json:"-"`
}

The main logic is implemented using these types, so I wrote an additional wrapper on top of that, which can be invoked by an external client which needs marshalled data. Here is an implementation example:

in the actor.gno file:


type Actor struct {
    Id      string      `json:"actorId"`
    Name    string      `json:"actorName"`
    Address std.Address `json:"actorAddress"`
}

var (
    Actors        []*Actor
    ActorTeams    map[string][]*Team
    ActorTasks    map[string][]*WorkableTask
    ActorProjects map[string][]*WorkableProject
)

// Initialize global maps
func init() {
    ActorTeams = make(map[string][]*Team)
    ActorTasks = make(map[string][]*WorkableTask)
    ActorProjects = make(map[string][]*WorkableProject)
}

// AddActor adds a new actor to the Actors slice
func (a *Actor) AddActor() error {
    if _, exist := GetActorById(a.Id); exist == nil {
        return errors.New("actor ID already exists")
    }
    Actors = append(Actors, a)
    return nil
}

in the actor_wrapper.gno file:

var currentUserID int

func init() {
    currentUserID = 0
}

// users

func AddActorWrap(userName string, userAddress string) error {
    userID := incrementUserID()
    a := &Actor{
        Id:          strconv.Itoa(userID),
        Name:        userName,
        Address:     std.Address(userAddress),
    }
    return a.AddActor()
}

ZenTasktic UI

The UI can be accessed at https://gno.zentasktic.com. There is a main dashboard with links for each realm UI. Underneath there is a next.js implementation, with gno-js library for calls and wallet interaction.

Screenshot 2024-09-04 at 12 30 23 PM

ZenTasktic Core Web UI

ZenTasktic Core UI (deployed in prod), with basic workflow:

Screenshot 2024-09-04 at 12 35 39 PM
Screenshot 2024-09-04 at 12 35 52 PM Screenshot 2024-09-04 at 12 36 30 PM Screenshot 2024-09-04 at 12 36 35 PM
Screenshot 2024-09-04 at 12 37 11 PM Screenshot 2024-09-04 at 12 37 28 PM Screenshot 2024-09-04 at 12 37 45 PM
Screenshot 2024-09-04 at 12 38 53 PM Screenshot 2024-09-04 at 12 39 02 PM

ZenTasktic Project Web UI

ZenTasktic Project web UI, with extra data types for Users, Teams and Rewards. Currently in testing.

Screenshot 2024-09-04 at 12 53 26 PM
Screenshot 2024-09-04 at 12 53 46 PM Screenshot 2024-09-04 at 12 54 02 PM
irreverentsimplicity commented 2 months ago

Finalized implementation for Users and Teams, with add, edit, remove functionality hooked to UI elements.

Screenshot 2024-09-11 at 5 36 11 PM Screenshot 2024-09-11 at 5 36 27 PM Screenshot 2024-09-11 at 5 36 43 PM

Finalized implementation for tasks assignment to teams (a task can be assigned to 1 or more teams).

Screenshot 2024-09-11 at 5 42 10 PM

Finalized implementation for rewards assignments to tasks (a task can be rewarded with arbitrary payments, for testing, we added GNOT, FLIP and ZEN tokens)

Screenshot 2024-09-11 at 5 42 49 PM