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:
- Tauri v2 - Version 2 of Tauri
- tauri-store-plugin - for persistent key-value storage
- tauri fs plugin
- tauri-plugin-sql - for persistent relational storage
- cors-fetch - for cors issues
- tauri dialog - for system dialogs (saving opening files on the OS)
- Pinia - for state management and use with tauri store
- Tailwind - for fast CSS styling
- Vue - frontend framework
- Vue Rotuer - page routing
Data Models
Task
A task to complete
Column | Type | Description |
---|---|---|
id | int | unique id for the task |
name | str | name of the task |
description | str | description of the task |
type | str | low,medium,high |
start_date | date | date started |
due_date | date | date due |
category_id | int (FK) | category for the task |
status | str | pending,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.
Column | Type | Description |
---|---|---|
id | int | unique id for the comment |
user_id | int (FK) | user that made the comment |
task_id | int (FK) | task the comment is on |
text | str | comment text of a user |
Category
Column | Type | Description |
---|---|---|
id | int | unique id for the category |
name | str | name of the category |
Status
The status of the task.
Column | Type | Description |
---|---|---|
id | int | unique id for the task |
label | str | pending,completed,failed |
description | str | amount 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.
Column | Type | Description |
---|---|---|
id | int | unique id for the user |
first_name | str | name of the user |
last_name | str | surname of the user |
str | email of the user | |
password | str | password |
profile_img | str | url 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.
Column | Type | Description |
---|---|---|
id | int | unique id for the user |
user_id | str | user the detail belongs to |
detail_1 | type | description |
detail_2 | type | description |
detail_3 | type | description |
detail_4 | type | description |
TaskMembers
Join table to assign users to tasks.
Column | Type | Description |
---|---|---|
id | int | unique id for the task |
task_id | int (FK) | task to be assigned to |
user_id | int (FK) | user assigned to task |
Note: All data models should have
updated_at
andcreated_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
- Task Ownership: Each task must be assigned to a specific user.
- Task Priority: Tasks can be assigned a priority level (e.g., low, medium, high, urgent) to guide workload management.
- Task Dependencies: Tasks can be linked to other tasks as dependencies, ensuring that certain tasks must be completed before others can begin.
Categories
- Category Hierarchy: Categories can be organized into a hierarchical structure, allowing for granular task classification.
- 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
- Comment Attachments: Users can attach files or images to comments for additional context or reference.
- Comment Notifications: Users can opt-in to receive notifications when new comments are added to tasks they're following or assigned to.
- Comment Moderation: Administrators can moderate comments to ensure they adhere to community guidelines and prevent abuse.
Additional Features
- Recurring Tasks: Tasks can be set to recur on a regular schedule (e.g., daily, weekly, monthly).
- 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
nvm use 18.18.0
yarn create tauri-app --rc
cd tauri-app
yarn
yarn tauri dev
Install dependencies
Yarn packages
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
yarn add @tauri-apps/plugin-sql
Note: postcss and autoprefixer may not be needed
https://tailwindcss.com/docs/installation/using-postcss
Cargo Packages
cd src-tauri
cargo add tauri-plugin-store
cargo add tauri-plugin-cors-fetch
cargo add tauri-plugin-sql --features sqlite
{
"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"
}
}
/** @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: [],
}
@tailwind base;
@tailwind components;
@tailwind utilities;
body{
background-color: rgb(31 41 55);
}
<!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>
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
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.
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)
},
}
});
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
[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"
// 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");
}
{
"$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/**" }]
}
]
}
{
"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
<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
<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
<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
...remove large file bash script
Starting from a Template
git clone https://github.com/...
cd ...