Skip to content
Shop

CommunityJoin Our PatreonDonate

Sponsored Ads

Sponsored Ads

Make a Tauri Crud App

I will explain how to make an app with full CRUD functionality using Tauri. This app will have a nice looking UI with one-to-one, one-to-many and many-to-many relationships.

What you will need:

Data Models

Task

A task to complete

ColumnTypeDescription
idintunique id for the task
namestrname of the task
descriptionstrdescription of the task
typestrlow,medium,high
start_datedatedate started
due_datedatedate due
category_idint (FK)category for the task
statusstrpending,completed,failed

Note: status could be an enumerable or another table

UserComments

A comment by a user on a task. This is a join table.

ColumnTypeDescription
idintunique id for the comment
user_idint (FK)user that made the comment
task_idint (FK)task the comment is on
textstrcomment text of a user

Category

ColumnTypeDescription
idintunique id for the category
namestrname of the category

Status

The status of the task.

ColumnTypeDescription
idintunique id for the task
labelstrpending,completed,failed
descriptionstramount of transaction

Users

A person who may responsible for a task. No auth will be added at first, but you will be able to add users to tasks. Auth can be added later.

ColumnTypeDescription
idintunique id for the user
first_namestrname of the user
last_namestrsurname of the user
emailstremail of the user
passwordstrpassword
profile_imgstrurl link to profile image (images won't be store locally)

UserDetails

Additional information about the user. Used to not pollute the user table with too many columns.

ColumnTypeDescription
idintunique id for the user
user_idstruser the detail belongs to
detail_1typedescription
detail_2typedescription
detail_3typedescription
detail_4typedescription

TaskMembers

Join table to assign users to tasks.

ColumnTypeDescription
idintunique id for the task
task_idint (FK)task to be assigned to
user_idint (FK)user assigned to task

Note: All data models should have updated_at and created_at fields

Relationships

How the models are related

  • A TASK belongs to a STATUS
  • A STATUS has many TASKS
  • A TASK can have many USERS through TASK_MEMBERS
  • A USER can have many TASKS through TASK_MEMBERS
  • A TASK can have many COMMENTS
  • A COMMENT has many TASKS
  • A TASK belongs to a CATEGORY
  • A CATEGORY has many TASKS
  • A TASK belongs to a CATEGORY
  • A USER has one USER_DETAIL
  • A USER_DETAIL has one USER

UI/UX

User Profile

Requirements

Functional Requirements

  • CRUD tasks
  • CRUD status
  • CRUD users
  • CRUD comments
  • CRUD statuses?
  • EXPORT tasks in a JSON format
  • View a log of events in the app

Non Functional Requirements

  • Useful - could be used for real work
  • Stable - No bugs
  • Beautiful - Nice looking UI

Technical Requirements

  • Desktop
  • Support Mac, Windows and PWA
  • Responsive

Business Rules

These business rules were generated by Google Gemini. Not all of these will be implemented.

Core Functionality

  1. Task Ownership: Each task must be assigned to a specific user.
  2. Task Priority: Tasks can be assigned a priority level (e.g., low, medium, high, urgent) to guide workload management.
  3. Task Dependencies: Tasks can be linked to other tasks as dependencies, ensuring that certain tasks must be completed before others can begin.

Categories

  1. Category Hierarchy: Categories can be organized into a hierarchical structure, allowing for granular task classification.
  2. Category Permissions: Users can have different levels of access to different categories, restricting or granting permission to create, edit, or view tasks within those categories.

Comments

  1. Comment Attachments: Users can attach files or images to comments for additional context or reference.
  2. Comment Notifications: Users can opt-in to receive notifications when new comments are added to tasks they're following or assigned to.
  3. Comment Moderation: Administrators can moderate comments to ensure they adhere to community guidelines and prevent abuse.

Additional Features

  1. Recurring Tasks: Tasks can be set to recur on a regular schedule (e.g., daily, weekly, monthly).
  2. Task Templates: Users can create pre-defined task templates to streamline the creation of common task types.

Create a new Tauri App

Create a new tauri app and run it

bash
nvm use 18.18.0
yarn create tauri-app --rc
cd tauri-app
yarn
yarn tauri dev

Install dependencies

Yarn packages

bash
yarn add tauri-plugin-store-api
yarn add pinia
yarn add vue-router
yarn add tailwindcss postcss autoprefixer
yarn run tauri add sql
yarn run tauri add fs
yarn run tauri add dialog
npx tailwindcss init

Manual way of adding sql plugin

bash
yarn add @tauri-apps/plugin-sql

Note: postcss and autoprefixer may not be needed

https://tailwindcss.com/docs/installation/using-postcss

Cargo Packages

bash
cd src-tauri
cargo add tauri-plugin-store
cargo add tauri-plugin-cors-fetch
cargo add tauri-plugin-sql --features sqlite
json
{
  "name": "tauri-template",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "tauri": "tauri",
    "build-tailwind": "npx tailwindcss -i ./src/assets/css/main.css -o ./src/assets/css/output.css --watch"
  },
  "dependencies": {
    "@tauri-apps/api": ">=2.0.0-rc.0",
    "@tauri-apps/plugin-shell": ">=2.0.0-rc.0",
    "autoprefixer": "^10.4.20",
    "pinia": "^2.2.2",
    "postcss": "^8.4.44",
    "tailwindcss": "^3.4.10",
    "tauri-plugin-store-api": "^0.0.0",
    "vue": "^3.3.4",
    "vue-router": "^4.4.3"
  },
  "devDependencies": {
    "@tauri-apps/cli": ">=2.0.0-rc.0",
    "@vitejs/plugin-vue": "^5.0.5",
    "vite": "^5.3.1"
  }
}
js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./src/components/**/*.{js,vue,ts}",
    "./src/layouts/**/*.vue",
    "./src/pages/**/*.vue",
    "./src/plugins/**/*.{js,ts}"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
js
@tailwind base;
@tailwind components;
@tailwind utilities;

body{
	background-color: rgb(31 41 55);
}
html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tauri + Vue 3 App</title>
    <link href="./src/assets/css/output.css" rel="stylesheet">
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
js
import { defineConfig } from "vite";
import { fileURLToPath, URL } from "url";
import vue from "@vitejs/plugin-vue";

const host = process.env.TAURI_DEV_HOST;

// https://vitejs.dev/config/
export default defineConfig(async () => ({
  plugins: [vue()],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./", import.meta.url)),
    },
  },

  // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
  //
  // 1. prevent vite from obscuring rust errors
  clearScreen: false,
  // 2. tauri expects a fixed port, fail if that port is not available
  server: {
    port: 1420,
    strictPort: true,
    host: host || false,
    hmr: host
      ? {
          protocol: "ws",
          host,
          port: 1421,
        }
      : undefined,
    watch: {
      // 3. tell vite to ignore watching `src-tauri`
      ignored: ["**/src-tauri/**"],
    },
  },
}));

Vue Setup

rust
import { dataStore } from '../stores/store'
import { createMemoryHistory, createRouter } from 'vue-router'
import { createPinia } from 'pinia'
import { createApp } from "vue";
import App from "./App.vue";
import "./assets/css/output.css"

import HomeView from './components/HomeView.vue'
import AboutView from './components/AboutView.vue'
import User from './components/User.vue'

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

const routes = [
    { path: '/', component: HomeView },
    { path: '/about', component: AboutView },
    { path: '/about', component: AboutView },
    { path: '/users/:name/:id', component: User },
  ]

const router = createRouter({
    history: createMemoryHistory(),
    routes,
  })

app.use(router)

router.beforeEach((to) => {
    // ✅ This will work because the router starts its navigation after
    // the router is installed and pinia will be installed too
    // const store = useCounterStore()
    // import { counter } from "../stores/counter";
    const store = dataStore();
    // alert('d')
  
    // if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
  })


app.mount('#app')

Pinia & Tauri Store

There are two ways you can set up a store.

rust
import { defineStore } from "pinia";
import { Store } from "tauri-plugin-store-api";

export const dataStore = defineStore("dataStore", {
  state: () => {
    return {
      store: new Store(".database.dat"),
      counter: 0,
      name: "ME",,
      cart: [],
      items: [
        { id: 1, name: "Blouses" },
        { id: 2, name: "Dresses" },
        { id: 3, name: "Denim & jeans" },
        { id: 4, name: "Knitwear" },
        { id: 5, name: "Pants" },
        { id: 5, name: "Skirts" },
        { id: 5, name: "Tops & tees" },
        { id: 5, name: "Jackets & coats" }
      ],
      products: 
        {
          shirts:[
        { id: 1, name: "shirts", url: "../src/assets/images/women/w_1.png" }
      ],
      pants:[
        { id: 1, name: "pants", url: "../src/assets/images/men/m_8.png" }]
    }
    }
  },
  getters: {
    doubleCount: (state) => state.counter * 2,
    oddOrEven: (state) => {
      if (state.counter % 2 === 0) return 'even'
      return 'odd'
    }
  },
  actions: {
    increment() {
      this.counter++
    },
    decrement() {
      if (this.counter > 0){
        this.counter--
      }
    },
    adToCart(x) {
      this.cart.push(x)
    },
  }
});
rust
import { defineStore } from 'pinia';
import { Store } from "tauri-plugin-store-api";
import { ref,computed } from 'vue';
export const dataStore = defineStore('counter', () => {
    const count = ref(0)
    const name = ref('Eduardo')
    const doubleCount = computed(() => count.value * 2)
    function increment() {
      count.value++
    }
    const store = new Store(".database.dat");

    return { store }
  })

Rust Setup

Note: add cors

rust
[package]
name = "tauri-template"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "tauri_template_lib"
crate-type = ["lib", "cdylib", "staticlib"]

[build-dependencies]
tauri-build = { version = "2.0.0-rc", features = [] }

[dependencies]
tauri = { version = "2.0.0-rc", features = [] }
tauri-plugin-shell = "2.0.0-rc"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-store = "2.0.0-rc.3"
tauri-plugin-cors-fetch = "2.1.1"
rust
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_cors_fetch::init())
        .plugin(tauri_plugin_sql::Builder::default().build()) //manual way
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
rust
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "requireLiteralLeadingDot": false,
  "windows": [
    "main"
  ],
  "permissions": [
    "core:default",
    "sql:allow-load",
    "sql:allow-execute",
    "sql:allow-select",
    "sql:allow-close",
    "shell:allow-open",
    "store:allow-get",
    "store:allow-set",
    "store:allow-save",
    "store:allow-load",
    "cors-fetch:default",
    "sql:default",
    "fs:default",
    "fs:allow-app-write",
    "fs:allow-exists",
    {
      "identifier": "fs:scope",
      "allow": [{ "path": "bar.txt" }, { "path": "$HOME/**" }]
    }
  ]
}
rust
{
  "productName": "tauri-template",
  "version": "0.1.0",
  "identifier": "com.tauri-template.app",
  "build": {
    "beforeDevCommand": "yarn dev",
    "devUrl": "http://localhost:1420",
    "beforeBuildCommand": "yarn build",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [
      {
        "title": "tauri-template",
        "width": 800,
        "height": 600
      }
    ],
    "withGlobalTauri": true,
    "security": {
      "csp": null
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  }
}

Using the store in a component

js
<script setup>
import { dataStore } from "@/stores/store";
import Home from "./components/Home.vue";
const store = dataStore();

</script>


<template>
  <div>
  ABOUT THIS PLACE

  <Home/>

  {{store}}
  </div>
</template>

Using Sqlite Database in a Vue Component

vue
<script setup>
import { dataStore } from "@/stores/store";
import { onMounted, ref } from 'vue'
import Database from '@tauri-apps/plugin-sql';
import { create, BaseDirectory, writeTextFile } from '@tauri-apps/plugin-fs';

import { save } from '@tauri-apps/plugin-dialog';

async function saveFile() {
  try {
    // Open the save file dialog
    const filePath = await save({
      title: 'Save File',
      defaultPath: 'data.json', // You can specify a default file name
      filters: [
        {
          name: 'Text Files',
          extensions: ['json', 'txt'],
        },
      ],
    });

    if (filePath) {
      // If the user selected a path, write the file
      await writeTextFile(filePath, `${JSON.stringify(user_data.value)}`);
      console.log('File saved to:', filePath);
    } else {
      console.log('File save cancelled');
    }
  } catch (error) {
    console.error('Failed to save file:', error);
  }
}

const store = dataStore();
// when using `"withGlobalTauri": true`, you may use
// const V = window.__TAURI_PLUGIN_SQL__;

const current_user = ref({});

const all_users = ref();

const user_data = ref({});

const editing = ref(false)

async function makeDatabase(){
  const db = await Database.load('sqlite:data.db');
  db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY,first_name TEXT NOT NULL,last_name TEXT NOT NULL,email TEXT NOT NULL UNIQUE,password TEXT NOT NULL,profile_image TEXT);');
  showMessage("Created the Database")
}

async function deleteUserTable(){
    const db = await Database.load('sqlite:data.db');
  db.execute("drop table users");
  showMessage("Deleted the Database")
}

function showMessage(msg){
  document.getElementById('status-message').classList.remove('hidden')
  setTimeout(()=>{document.getElementById('status-message').classList.add('hidden')}, 5000)
  document.getElementById('status-message').innerHTML = msg
}


async function createUser(x){
  const db = await Database.load('sqlite:data.db');
  db.execute("INSERT INTO users (first_name,last_name,email,password,profile_image) VALUES ($1,$2,$3,$4,$5)", x);
}

async function createUserForm(){
  const db = await Database.load('sqlite:data.db');
  const fn = user_data.value['first_name']
  const ln = user_data.value['last_name']
  const em = user_data.value['email']
  const pass = user_data.value['password']
  const prof = user_data.value['profile_image']
  db.execute("INSERT INTO users (first_name,last_name,email,password,profile_image) VALUES ($1,$2,$3,$4,$5)", [fn,ln,em,pass,prof]);
  document.getElementById('theform').reset()
  getAllUsers()
}

async function getAllUsers(){
  const db = await Database.load('sqlite:data.db');
 const result = await db.select(
 "SELECT * from users");
 all_users.value = result
}

function loadUser(evt,user){
  user_data.value=user
  current_user.value = user
  editing.value = true
}

function clearForm(){
  document.getElementById('theform').reset()
  user_data.value = {}
  editing.value = false
}

async function getUserById(x){
  const db = await Database.load('sqlite:data.db');
 const result = await db.select(
 "SELECT * from users WHERE id = $1", [x]);
 current_user.value = result
}

async function  exportUsers(){
  console.log(BaseDirectory.App)
  const file = await create('data.json', { baseDir: BaseDirectory.App });
  await file.write(new TextEncoder().encode(`${JSON.stringify(user_data.value)}`));
  showMessage("Exported the file to data.json")
  await file.close();
}

async function deleteUserById(x){
  const db = await Database.load('sqlite:data.db');
 const result = await db.select(
 "delete from users WHERE id = $1", [x]);
 current_user.value = null
 getAllUsers()
}

async function updateUserById(x, data){
  const db = await Database.load('sqlite:data.db');
  const fn = user_data.value['first_name']
  const ln = user_data.value['last_name']
  const em = user_data.value['email']
  const pass = user_data.value['password']
  const prof = user_data.value['profile_image']
  try{
  const result = await db.execute(
   "UPDATE users SET first_name = $1, last_name = $2, email = $3, password = $4, profile_image = $5 WHERE id = $6",
   [fn,ln,em,pass,prof,x]
  );
  clearForm()
  getAllUsers()
  }catch{
    console.log("Couldn't update. Check your form")
  }
  // document.getElementById('theform').reset()
}

onMounted(() => {
  // makeDatabase()
  // deleteUserTable()
  console.log("I AM MOUNTED AT LEAST");
  // createUser(['rachel','sanders','rachel@gmail.com', 'password', 'https://image.lexica.art/full_webp/e99c77f3-1201-4ec1-a51a-3f6bede109c4'])
  // getUserById(1)
  getAllUsers();
  // deleteUserById(3)
  user_data.value = {
    first_name: "Default",
    last_name:"Default",
    email:"Default",
    password:"password",
    profile_image:"Default",
  }
})

</script>


<template>
  <div class="bg-gray-100 h-screen">
<div class="p-8 flex w-full gap-4 h-full">
    <aside class=" bg-white rounded-lg lg:w-[400px]">
    <form id="theform" class="py-3  space-y-2 px-3 rounded-lg">
        <div class="rounded-lg">
            <label>First Name</label></br>
            <input v-model="user_data['first_name']" type="text" class="w-full mt-2 px-2 py-1 bg-gray-100 rounded-md outline-none border-none" value="hi"/>
        </div>

        <div class="rounded-lg">
            <label>Last Name</label></br>
            <input v-model="user_data['last_name']" type="text" class="w-full mt-2 px-2 py-1 bg-gray-100 rounded-md outline-none border-none" value="hi"/>
        </div>

        <div class="rounded-lg">
            <label>Email</label></br>
            <input  v-model="user_data['email']" type="text" class="w-full mt-2 px-2 py-1 bg-gray-100 rounded-md outline-none border-none" value="hi"/>
        </div>

        <div class="rounded-lg">
            <label>Password</label></br>
            <input  v-model="user_data['password']" type="text" class="w-full mt-2 px-2 py-1 bg-gray-100 rounded-md outline-none border-none" value="hi"/>
        </div>

        <div class="rounded-lg">
            <label>Profile Image</label></br>
            <input  v-model="user_data['profile_image']" type="text" class="w-full mt-2 px-2 py-1 bg-gray-100 rounded-md outline-none border-none" value="hi"/>
        </div>

        <input  v-if="editing" v-model="user_data['id']" type="hidden" class="w-full mt-2 px-2 py-1 bg-gray-100 rounded-md outline-none border-none" value="hi"/>

        <div class="flex justify-end">
        <button v-if="!editing" class="bg-blue-500 text-white rounded-lg p-2"  @click.prevent="createUserForm()">Submit</button>
        <button v-if="editing" class="bg-blue-500 text-white rounded-lg p-2" @click.prevent="updateUserById(user_data['id'])">UPDATE</button>
        <button class="ml-2 border text-red-500 border-red-500 rounded-lg p-2"  @click.prevent="clearForm()">Clear</button>
        </div>
        
    </form>
    <div class="my-2 px-3 w-full">
    <button id="create-database-button" class="mt-2 border w-full text-green-500 border-green-500 rounded-lg p-2"  @click.prevent="makeDatabase()">Create Database</button>
    <button id="create-database-button" class="mt-2 border w-full text-red-500 border-red-500 rounded-lg p-2"  @click.prevent="deleteUserTable()">Delete Database</button>
    <button id="create-database-button" class="mt-2 border w-full text-red-500 border-red-500 rounded-lg p-2"  @click.prevent="exportUsers()">Export</button>
    <button id="create-database-button" class="mt-2 border w-full text-red-500 border-red-500 rounded-lg p-2"  @click.prevent="saveFile()">Save</button>
    </div>
    </aside>
    <main class="w-full">
        <div class="px-2">
            <section class=" bg-white p-3 rounded-lg">
                <div class="rounded-lg relative h-[200px] bg-gradient-to-r from-cyan-500 to-blue-500 ">
                <img class="mx-8 -mb-6 absolute bottom-0 left-0 w-14 h-14 border border-2 object-cover rounded-full" :src="user_data['profile_image']">
                </div>
                <div class="mt-10 flex">
                    <p class="inline-block capitalize">{{user_data['first_name']}}</p>
                    <p class="ml-2 inling-block capitalize">{{user_data['last_name']}}</p>
                </div>
                <div>Lso Angeles, United States</div> 
                <div>{{user_data['email']}}</div> 
                <div>@amanda21 - Lead product designer at Google - Full time</div> 
                <div class="mt-2 space-x-4">
                    <button class="bg-blue-500 text-white rounded-lg p-2">Message</button>
                    <button class="border text-blue-500 border-blue-500 rounded-lg p-2">Share</button>
                </div> 
            </section>

            <section class="bg-white mt-4 p-3 rounded-lg">
                <p class=" font-semibold">Skills</p>
                <div class="mt-2 grid grid-cols-5 gap-3 ">
                    <p v-for="i in 10" class="border border-gray-200 rounded-md flex items-center justify-center">
                        skill
                    </p>   
                </div> 
            </section>

            <section class="bg-white mt-4 p-3 rounded-lg">
                <p class=" font-semibold">Employment history</p>
                <div class="mt-2 grid grid-cols-5 gap-3 ">
                    <p v-for="i in 10" class="border border-gray-200 rounded-md flex items-center justify-center">
                        skill
                    </p>   
                </div> 
            </section>
        </div>
    </main>

    <aside class="bg-purple-gray-200 lg:w-[400px]">
        <ul class="space-y-2 px-2">
            <li @click="loadUser($event, user)" class="w-full justify-start p-2  rounded-lg bg-white flex items-center capitalize" v-for="(user, ndx) in all_users" :key="user.id">
                
                <span><img class="w-4 h-4 object-cover rounded-full" :src="user.profile_image"></span>
                <span class="ml-2">{{user.first_name}}</span>
                <span class="ml-2">{{user.last_name}}</span>
                <span class="ml-2">{{user.id}}</span>

                <button class="ml-auto" @click="deleteUserById(user.id)">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
                    <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
                    <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
                  </svg>
                </button>
            </li>
        </ul>
    </aside>
</div>

</div>
</template>

Tasks Component

vue
<script setup>
import { dataStore } from "@/stores/store";
import { onMounted, ref } from 'vue'
import Database from '@tauri-apps/plugin-sql';

const store = dataStore();
// when using `"withGlobalTauri": true`, you may use
// const V = window.__TAURI_PLUGIN_SQL__;

const current_task = ref({});

const payload = ref({name: "Tauri Prototype", description: "Creating the prototype for the tauri TODO app. This is created in a single Vue file.",start_date: "2024-09-05T13:43", end_date: "2024-09-09T15:45", category: 1, status: 1, type:1})

const all_tasks = ref();

const task_users = ref();

const message = ref('Create a Task');

const  task_data = ref({});

const editing = ref(false)

async function makeDatabase(){
  const db = await Database.load('sqlite:data.db');

  db.execute('CREATE TABLE IF NOT EXISTS types (id INTEGER PRIMARY KEY,name TEXT NOT NULL UNIQUE);');

  db.execute('CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY,name TEXT NOT NULL UNIQUE);');

  db.execute('CREATE TABLE IF NOT EXISTS statuses (id INTEGER PRIMARY KEY,name TEXT NOT NULL UNIQUE, description TEXT NOT NULL);');

  db.execute('CREATE TABLE IF NOT EXISTS task_users (task_id INTEGER, user_id INTEGER, FOREIGN KEY(task_id) REFERENCES tasks(id), FOREIGN KEY(user_id) REFERENCES users(id));');

  db.execute(`
  CREATE TABLE IF NOT EXISTS tasks (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    start_date TEXT,
    due_date TEXT,
    category INTEGER,
    status INTEGER,
    type INTEGER,
    FOREIGN KEY(category) REFERENCES categories(id),
    FOREIGN KEY(status) REFERENCES statuses(id),
    FOREIGN KEY(type) REFERENCES types(id));`);
}

async function createType(x){
  const db = await Database.load('sqlite:data.db');
  db.execute("INSERT INTO types (name) VALUES ($1)", [x]);
}

async function createCategory(x){
  const db = await Database.load('sqlite:data.db');
  db.execute("INSERT INTO categories (name) VALUES ($1)", [x]);
}

async function createStatus(x){
  const db = await Database.load('sqlite:data.db');
  db.execute("INSERT INTO statuses (name, description) VALUES ($1,$2)", x);
}

async function createTask(x){
  const db = await Database.load('sqlite:data.db');
  db.execute("INSERT INTO tasks (name,description,start_date,due_date,category,status,type) VALUES ($1,$2,$3,$4,$5,$6,$7)", x);
}

async function createTaskUsers(x){
  const db = await Database.load('sqlite:data.db');
  db.execute("INSERT INTO task_users (task_id,user_id) VALUES ($1,$2)", x);
}


async function deleteTables(){
    const db = await Database.load('sqlite:data.db');
  db.execute("drop table tasks");
  db.execute("drop table categories");
  db.execute("drop table types");
  db.execute("drop table statuses");
  db.execute("drop table task_users");
}


async function createTaskForm(){
  const db = await Database.load('sqlite:data.db');
  const name =  task_data.value['name']
  const description =  task_data.value['description']
  const start_date =  task_data.value['start_date']
  const due_date =  task_data.value['due_date']
  const category =  task_data.value['category']
  const status =  task_data.value['status']
  const type =  task_data.value['type']
  db.execute("INSERT INTO tasks (name,description,start_date,due_date,category,status,type) VALUES ($1,$2,$3,$4,$5,$6,$7)", [name,description,start_date,due_date,category,status,type]);
  document.getElementById('theform').reset()
  getAllTasks()
}

async function getAllTasks(){
  const db = await Database.load('sqlite:data.db');
 const result = await db.select(
 "SELECT * from tasks");
 all_tasks.value = result
}

async function getTaskUsers(){
  const db = await Database.load('sqlite:data.db');
 const result = await db.select(
 "SELECT * from task_users");
 task_users.value = result
}

function loadUser(evt,task){
   task_data.value=task
  current_task.value = task
  editing.value = true
}

function clearForm(){
  document.getElementById('theform').reset()
   task_data.value = {}
  editing.value = false
}

async function getTaskById(x){
  const db = await Database.load('sqlite:data.db');
 const result = await db.select(
 "SELECT * from tasks WHERE id = $1", [x]);
 current_task.value = result
}

async function deleteTaskById(x){
  const db = await Database.load('sqlite:data.db');
 const result = await db.select(
 "delete from tasks WHERE id = $1", [x]);
 current_task.value = null
 getAllTasks()
}

function seed(){
    createCategory("Development");
    createStatus(["Pending", "awaiting something"]);
    createType("Low");
    createTask(['task1','the first task','2024-09-09T15:45','2024-09-09T15:45',1,1,1])
    createTaskUsers([1,1])
    // getTaskUsers()
    getAllTasks()
}

async function updateTaskById(x, data){
  const db = await Database.load('sqlite:data.db');
  const name =  task_data.value['name']
  const description =  task_data.value['description']
  const start_date =  task_data.value['start_date']
  const due_date =  task_data.value['due_date']
  const category =  task_data.value['category']
  const status =  task_data.value['status']
  const type =  task_data.value['type']
  try{
  const result = await db.execute(
   "UPDATE tasks SET name = $1, description = $2, start_date = $3, due_date = $4, category = $5, status = $6, type = $7, WHERE id = $8",
   [name,description,start_date,due_date,category,status,type,x]
  );
  clearForm()
  getAllTasks()
  }catch{
    console.log("Couldn't update. Check your form")
  }
  // document.getElementById('theform').reset()
}

onMounted(() => {
  makeDatabase()
//   deleteTables()
  console.log("I AM MOUNTED AT LEAST");
  //getTaskById(1)
  getAllTasks();
  // deleteTaskById(3)
   task_data.value = {
    name: "Task Default",
    description: "description",
    start_date: "2024-09-05T13:43",
    due_date: "2024-09-05T13:43",
    category: 1,
    status: 1,
    type: 1
  }
})

</script>


<template>
  <div class="bg-gray-100 h-full">
<div class="p-8 flex w-full gap-4 h-full">
    <aside class=" bg-white rounded-lg lg:w-[400px]">
    <div class="py-3  space-y-2 px-3 rounded-lg">
       <button @click="seed()">Seed</button>
    </div>
    </aside>
    
    <main class="w-full">
        <div class="px-2">
            <section class=" bg-white p-3 rounded-lg">

                <div class="container mx-auto">
        <nav class="text-white py-4 w-full">
            <ul class="flex justify-between w-full">
                <li><button></button></li>
                <li class="text-sm">{{message}}</li>
                <li><button></button></li>
            </ul>
        </nav>

        <div class="flex w-full">
            <!-- MAIN -->
            <main class="md:mx-4 w-full md:w-10/12">
                <form id="theform" class="p-4 bg-white rounded-xl">
                    <div>
                        <label class="font-semibold text-zinc-700" for="category">Label</label><br>
                        <div class="mt-2 p-1 w-full bg-zinc-100 rounded-lg">
                        <select v-model="payload.category" class="px-2 outline-none text-zinc-500 w-full bg-zinc-100 p-1 rounded-lg">
                            <option>UI Design</option>
                            <option>Development</option>
                        </select>
                        </div>
                    </div>
                    <div class="mt-2">
                        <label class="font-semibold text-zinc-700" for="name">Task Name</label><br>
                        <input v-model="payload.name" class="py-2 px-4 text-zinc-500 mt-2 w-full bg-zinc-100 p-1 rounded-lg" name="name" id="name"/>
                    </div>

                    <div class="mt-2">
                        <label class="font-semibold text-zinc-700" for="description">Description</label><br>
                        <input v-model="payload.description" class="py-2 px-4 text-zinc-500 mt-2 w-full bg-zinc-100 p-1 rounded-lg" name="description" id="name"/>
                    </div>

                    <div class="mt-2">
                        <label class="font-semibold text-zinc-700" for="type">Task Type</label><br>
                        <div class="mt-2">
                        <input value="Low" type="button" class="px-2 rounded-md inline-block bg-indigo-100 border border-indigo-100 text-indigo-600" name="type" id="type"/>
                        <input value="Medium" type="button" class="px-2 rounded-md mx-2 inline-block bg-orange-100 border border-orange-100 text-orange-400" name="type" id="type"/>
                        <input value="High" type="button" class="px-2 rounded-md inline-block bg-red-100 border border-red-100 text-red-600" name="type" id="type"/>
                        </div>
                    </div>

                    <div class="mt-2">
                        <label class="font-semibold text-zinc-700" for="name">Choose Date & Time</label><br>
                        <div class="grid grid-cols-1 md:grid-cols-2 gap-x-2">
                        <input v-model="payload.start_date" type="datetime-local" class="mt-2 py-2 outline-none text-zinc-500 w-full bg-zinc-100 p-1 rounded-lg" name="start_date" id="start_date"/>
                        <input v-model="payload.end_date" type="datetime-local" class="mt-2 py-2 outline-none text-zinc-500 w-full bg-zinc-100 p-1 rounded-lg" name="end_date" id="end_date"/>
                        </div>
                    </div>

                    <div class="mt-2">
                        <label class="font-semibold text-zinc-700" for="name">Team Members</label><br>
                        <div class="mt-2 flex flex-wrap justify-start gap-2">
                            <div v-for="i in 6" class="px-1 py-1 rounded-lg border border-zinc-50 bg-white shadow-md flex items-center">
                                    <img class="w-4 h-4 object-cover rounded-full " src="https://image.lexica.art/full_webp/e99c77f3-1201-4ec1-a51a-3f6bede109c4"/>
                                    <small class="ml-2">FName LName</small>
                            </div>
                        </div>
                    </div>

                    <div class="mt-2">
                        <label class="font-semibold text-zinc-700" for="name">Description</label><br>
<textarea v-model="payload.description" class="outline-none mt-2 border border-zinc-100 w-full p-3 text-zinc-400 rounded-lg" rows="10">
</textarea>
                    </div>

                </form>
                <div class="mt-4 p-4 bg-white rounded-lg">
                    <p class="break-words">{{payload}}</p>
                    <hr class="my-3"/>

                    <p class="break-words">{{all_tasks}}</p>
                </div>
            </main>
            <!-- /MAIN -->
        </div>

    </div>
                
            </section>

            <section class="bg-white mt-4 p-3 rounded-lg">
                <p class=" font-semibold">Skills</p>
            </section>
        </div>
    </main>

    <aside class=" bg-gray-100 rounded-lg lg:w-[400px]">
        <ul class="space-y-2 px-2">
            <li @click="loadUser($event, task)" class="w-full justify-start p-2  rounded-lg bg-white flex items-center capitalize" v-for="(task, ndx) in all_tasks" :key="task.id">
                
                <span class="ml-2">{{task.name}}</span>
                <span class="ml-2">{{task.description}}</span>
                <!-- <span class="ml-2">{{task.id}}</span> -->
                 

                <button class="ml-auto" @click="deleteTaskById(task.id)">
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
                    <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/>
                    <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/>
                  </svg>
                </button>
            </li>
        </ul>
    </aside>
</div>

</div>
</template>

Build the App for release

yarn tauri build

Notes

  • For Vue, @click="clearPrompt(this)" has to be @click="clearPrompt($event)"
  • Building produces large files (gigs)
  • nvm use 18.1.8.0

Clean up large files

rust
...remove large file bash script

Starting from a Template

git clone https://github.com/...
cd ...

Resources