Adding Features Workflow

Step-by-step guide to adding new features to DesQTA

Feature Development Workflow

This guide walks you through the complete process of adding a new feature to DesQTA, from planning to deployment.

Step 1: Planning

Define the Feature

  1. What problem does it solve?
  2. Who will use it?
  3. What are the requirements?
  4. What are the constraints?

Create Feature Branch

git checkout develop
git pull origin develop
git checkout -b feature/my-feature-name

Step 2: Architecture Design

Frontend Design

  • Which route? (/new-feature)
  • What components? (List, Card, Detail, etc.)
  • What state? (Component state, store, service)
  • What services? (API calls, data transformation)

Backend Design

  • What Tauri commands? (get_feature_data, save_feature_data)
  • What Rust modules? (utils/feature.rs)
  • What data structures? (Structs, enums)

Step 3: Implementation

Backend First

  1. Create Rust module
// src-tauri/src/utils/feature.rs

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct FeatureData {
    pub id: i32,
    pub name: String,
    pub value: String,
}

#[tauri::command]
pub async fn get_feature_data() -> Result<Vec<FeatureData>, String> {
    // Implementation
    Ok(vec![])
}

#[tauri::command]
pub async fn save_feature_data(data: FeatureData) -> Result<(), String> {
    // Implementation
    Ok(())
}
  1. Register commands
// src-tauri/src/lib.rs
mod utils;
use utils::feature::*;

.invoke_handler(tauri::generate_handler![
    get_feature_data,
    save_feature_data,
])

Frontend Implementation

  1. Create route
<!-- src/routes/new-feature/+page.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { invoke } from '@tauri-apps/api/core';
  import { useDataLoader } from '$lib/utils/useDataLoader';
  
  let items = $state<FeatureData[]>([]);
  let loading = $state(true);
  
  onMount(async () => {
    const data = await useDataLoader({
      cacheKey: 'feature_data',
      ttlMinutes: 10,
      context: 'feature',
      functionName: 'loadFeatureData',
      fetcher: async () => {
        return await invoke<FeatureData[]>('get_feature_data');
      },
      onDataLoaded: (data) => {
        items = data;
        loading = false;
      }
    });
  });
</script>

{#if loading}
  <LoadingSpinner />
{:else}
  {#each items as item (item.id)}
    <FeatureCard {item} />
  {/each}
{/if}
  1. Create service
// src/lib/services/featureService.ts
import { invoke } from '@tauri-apps/api/core';

export interface FeatureData {
  id: number;
  name: string;
  value: string;
}

export const featureService = {
  async load(): Promise<FeatureData[]> {
    return await invoke<FeatureData[]>('get_feature_data');
  },
  
  async save(data: FeatureData): Promise<void> {
    await invoke('save_feature_data', { data });
  }
};
  1. Create components
<!-- src/lib/components/FeatureCard.svelte -->
<script lang="ts">
  interface Props {
    item: FeatureData;
  }
  
  let { item }: Props = $props();
</script>

<div class="feature-card">
  <h3>{item.name}</h3>
  <p>{item.value}</p>
</div>

Step 4: Testing

Manual Testing

  1. Test happy path
  2. Test error cases
  3. Test edge cases
  4. Test on different platforms

Automated Testing

// featureService.test.ts
import { describe, it, expect } from 'vitest';
import { featureService } from './featureService';

describe('featureService', () => {
  it('loads feature data', async () => {
    const data = await featureService.load();
    expect(Array.isArray(data)).toBe(true);
  });
});

Step 5: Documentation

Update User Docs

Add to user guide if feature is user-facing:

## New Feature

Description of how to use the feature...

Update Developer Docs

Document the implementation:

## Feature Implementation

How the feature works internally...

Step 6: Code Review

Before Submitting PR

  • Code follows style guide
  • All tests pass
  • Documentation updated
  • No console.logs or debug code
  • Error handling implemented
  • TypeScript types correct

PR Checklist

## Description
Brief description of the feature

## Changes
- Added new route `/new-feature`
- Created `FeatureCard` component
- Added `get_feature_data` Tauri command

## Testing
- [ ] Tested on Windows
- [ ] Tested on macOS
- [ ] Tested on Linux
- [ ] Manual testing completed

## Screenshots
(if applicable)

Step 7: Merge and Deploy

Merge Process

  1. Create PR to develop branch
  2. Address review comments
  3. Merge when approved
  4. Deploy to staging (if applicable)
  5. Deploy to production (when ready)

Example: Adding a Notes Feature

Backend

// src-tauri/src/utils/notes.rs

#[tauri::command]
pub fn save_note(note_id: String, content: String) -> Result<(), String> {
    let notes_dir = get_notes_directory()?;
    let file_path = notes_dir.join(format!("{}.md", note_id));
    fs::write(&file_path, content)
        .map_err(|e| format!("Failed to save note: {}", e))
}

#[tauri::command]
pub fn load_note(note_id: String) -> Result<String, String> {
    let notes_dir = get_notes_directory()?;
    let file_path = notes_dir.join(format!("{}.md", note_id));
    fs::read_to_string(&file_path)
        .map_err(|e| format!("Failed to load note: {}", e))
}

Frontend Service

// src/lib/services/notesService.ts
export class NotesService {
  async saveNote(id: string, content: string): Promise<void> {
    await invoke('save_note', { note_id: id, content });
  }
  
  async loadNote(id: string): Promise<string | null> {
    try {
      return await invoke<string>('load_note', { note_id: id });
    } catch {
      return null;
    }
  }
}

export const notesService = new NotesService();

Component

<!-- src/routes/notes/+page.svelte -->
<script lang="ts">
  import { notesService } from '$lib/services/notesService';
  import { onMount } from 'svelte';
  
  let content = $state('');
  let noteId = $state('note-1');
  
  onMount(async () => {
    const loaded = await notesService.loadNote(noteId);
    if (loaded) {
      content = loaded;
    }
  });
  
  async function save() {
    await notesService.saveNote(noteId, content);
  }
</script>

<textarea bind:value={content} />
<button onclick={save}>Save</button>

Best Practices

1. Start Small

// ✅ Good - Minimal viable feature
async function loadData() {
  return await invoke('get_data');
}

// ❌ Avoid - Over-engineered
async function loadDataWithRetryAndCacheAndValidation() {
  // Too complex for first version
}

2. Follow Existing Patterns

// ✅ Good - Use existing patterns
const data = await useDataLoader({
  cacheKey: 'data',
  fetcher: async () => await invoke('get_data')
});

// ❌ Avoid - Reinventing patterns
const data = await customFetchWithCustomCache();

3. Error Handling

// ✅ Good - Always handle errors
try {
  const data = await loadData();
} catch (error) {
  logger.error('feature', 'loadData', `Failed: ${error}`, { error });
  showError(error);
}

// ❌ Avoid - Ignoring errors
const data = await loadData(); // What if it fails?

Next Steps