mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-22 19:14:31 +03:00
make git information reactive
This commit is contained in:
parent
1f70f359d7
commit
88c731c50a
3
.gitignore
vendored
3
.gitignore
vendored
@ -28,3 +28,6 @@ dist-ssr
|
||||
/package
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
# gitbutler
|
||||
.git/gb-*
|
||||
|
@ -15,14 +15,27 @@ impl Event {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn git(project: &projects::Project) -> Self {
|
||||
let event_name = format!("project://{}/git", project.id);
|
||||
let payload = serde_json::json!({
|
||||
"logs/HEAD": "updated",
|
||||
});
|
||||
pub fn git_index(project: &projects::Project) -> Self {
|
||||
let event_name = format!("project://{}/git/index", project.id);
|
||||
Event {
|
||||
name: event_name,
|
||||
payload: payload,
|
||||
payload: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn git_head(project: &projects::Project, head: &str) -> Self {
|
||||
let event_name = format!("project://{}/git/head", project.id);
|
||||
Event {
|
||||
name: event_name,
|
||||
payload: serde_json::json!({ "head": head }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn git_activity(project: &projects::Project) -> Self {
|
||||
let event_name = format!("project://{}/git/activity", project.id);
|
||||
Event {
|
||||
name: event_name,
|
||||
payload: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
|
3
src-tauri/src/git/mod.rs
Normal file
3
src-tauri/src/git/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod activity;
|
||||
#[cfg(test)]
|
||||
mod activity_tests;
|
@ -1,6 +1,7 @@
|
||||
mod deltas;
|
||||
mod events;
|
||||
mod fs;
|
||||
mod git;
|
||||
mod projects;
|
||||
mod repositories;
|
||||
mod search;
|
||||
@ -14,6 +15,7 @@ extern crate log;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use deltas::Delta;
|
||||
use git::activity;
|
||||
use serde::{ser::SerializeMap, Serialize};
|
||||
use std::{collections::HashMap, ops::Range, sync::Mutex};
|
||||
use storage::Storage;
|
||||
@ -373,12 +375,26 @@ async fn list_deltas(
|
||||
Ok(deltas)
|
||||
}
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
#[tauri::command(async)]
|
||||
async fn git_activity(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
start_time_ms: Option<u128>,
|
||||
) -> Result<Vec<activity::Activity>, Error> {
|
||||
let repo = repo_for_project(handle, project_id)?;
|
||||
let activity = repo
|
||||
.activity(start_time_ms)
|
||||
.with_context(|| "Failed to get git activity")?;
|
||||
Ok(activity)
|
||||
}
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
#[tauri::command(async)]
|
||||
async fn git_status(
|
||||
handle: tauri::AppHandle,
|
||||
project_id: &str,
|
||||
) -> Result<HashMap<String, String>, Error> {
|
||||
) -> Result<HashMap<String, repositories::FileStatus>, Error> {
|
||||
let repo = repo_for_project(handle, project_id)?;
|
||||
let files = repo.status().with_context(|| "Failed to get git status")?;
|
||||
Ok(files)
|
||||
@ -454,12 +470,12 @@ async fn git_branches(handle: tauri::AppHandle, project_id: &str) -> Result<Vec<
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
#[tauri::command(async)]
|
||||
async fn git_branch(handle: tauri::AppHandle, project_id: &str) -> Result<String, Error> {
|
||||
async fn git_head(handle: tauri::AppHandle, project_id: &str) -> Result<String, Error> {
|
||||
let repo = repo_for_project(handle, project_id)?;
|
||||
let files = repo
|
||||
.branch()
|
||||
let head = repo
|
||||
.head()
|
||||
.with_context(|| "Failed to get the git branch ref name")?;
|
||||
Ok(files)
|
||||
Ok(head)
|
||||
}
|
||||
|
||||
#[timed(duration(printer = "debug!"))]
|
||||
@ -611,9 +627,10 @@ fn main() {
|
||||
get_user,
|
||||
search,
|
||||
git_status,
|
||||
git_activity,
|
||||
git_match_paths,
|
||||
git_branches,
|
||||
git_branch,
|
||||
git_head,
|
||||
git_switch_branch,
|
||||
git_commit,
|
||||
git_wd_diff,
|
||||
|
@ -1,7 +1,7 @@
|
||||
mod repository;
|
||||
mod storage;
|
||||
|
||||
pub use repository::Repository;
|
||||
pub use repository::{Repository, FileStatus};
|
||||
pub use storage::Store;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{deltas, fs, projects, sessions, users};
|
||||
use crate::{deltas, git::activity, projects, sessions, users};
|
||||
use anyhow::{Context, Result};
|
||||
use git2::{BranchType, Cred, DiffOptions, Signature};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env, fs as std_fs,
|
||||
@ -10,6 +11,17 @@ use std::{
|
||||
use tauri::regex::Regex;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FileStatus {
|
||||
Added,
|
||||
Modified,
|
||||
Deleted,
|
||||
Renamed,
|
||||
TypeChange,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Repository {
|
||||
pub project: projects::Project,
|
||||
@ -124,12 +136,14 @@ impl Repository {
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
// return current branch name
|
||||
pub fn branch(&self) -> Result<String> {
|
||||
// return current head name
|
||||
pub fn head(&self) -> Result<String> {
|
||||
let repo = self.git_repository.lock().unwrap();
|
||||
let head = repo.head()?;
|
||||
let branch = head.name().unwrap();
|
||||
Ok(branch.to_string())
|
||||
Ok(head
|
||||
.name()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("undefined".to_string()))
|
||||
}
|
||||
|
||||
// return file contents for path in the working directory
|
||||
@ -214,8 +228,36 @@ impl Repository {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn activity(&self, start_time_ms: Option<u128>) -> Result<Vec<activity::Activity>> {
|
||||
let head_logs_path = Path::new(&self.project.path)
|
||||
.join(".git")
|
||||
.join("logs")
|
||||
.join("HEAD");
|
||||
|
||||
if !head_logs_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let activity = std::fs::read_to_string(head_logs_path)
|
||||
.with_context(|| "failed to read HEAD logs")?
|
||||
.lines()
|
||||
.filter_map(|line| activity::parse_reflog_line(line).ok())
|
||||
.collect::<Vec<activity::Activity>>();
|
||||
|
||||
let activity = if let Some(start_timestamp_ms) = start_time_ms {
|
||||
activity
|
||||
.into_iter()
|
||||
.filter(|activity| activity.timestamp_ms > start_timestamp_ms)
|
||||
.collect::<Vec<activity::Activity>>()
|
||||
} else {
|
||||
activity
|
||||
};
|
||||
|
||||
Ok(activity)
|
||||
}
|
||||
|
||||
// get file status from git
|
||||
pub fn status(&self) -> Result<HashMap<String, String>> {
|
||||
pub fn status(&self) -> Result<HashMap<String, FileStatus>> {
|
||||
let mut options = git2::StatusOptions::new();
|
||||
options.include_untracked(true);
|
||||
options.include_ignored(false);
|
||||
@ -235,19 +277,19 @@ impl Repository {
|
||||
let path = entry.path().unwrap();
|
||||
// get the status as a string
|
||||
let istatus = match entry.status() {
|
||||
s if s.contains(git2::Status::WT_NEW) => "added",
|
||||
s if s.contains(git2::Status::WT_MODIFIED) => "modified",
|
||||
s if s.contains(git2::Status::WT_DELETED) => "deleted",
|
||||
s if s.contains(git2::Status::WT_RENAMED) => "renamed",
|
||||
s if s.contains(git2::Status::WT_TYPECHANGE) => "typechange",
|
||||
s if s.contains(git2::Status::INDEX_NEW) => "added",
|
||||
s if s.contains(git2::Status::INDEX_MODIFIED) => "modified",
|
||||
s if s.contains(git2::Status::INDEX_DELETED) => "deleted",
|
||||
s if s.contains(git2::Status::INDEX_RENAMED) => "renamed",
|
||||
s if s.contains(git2::Status::INDEX_TYPECHANGE) => "typechange",
|
||||
_ => "other",
|
||||
git2::Status::WT_NEW => FileStatus::Added,
|
||||
git2::Status::WT_MODIFIED => FileStatus::Modified,
|
||||
git2::Status::WT_DELETED => FileStatus::Deleted,
|
||||
git2::Status::WT_RENAMED => FileStatus::Renamed,
|
||||
git2::Status::WT_TYPECHANGE => FileStatus::TypeChange,
|
||||
git2::Status::INDEX_NEW => FileStatus::Added,
|
||||
git2::Status::INDEX_MODIFIED => FileStatus::Modified,
|
||||
git2::Status::INDEX_DELETED => FileStatus::Deleted,
|
||||
git2::Status::INDEX_RENAMED => FileStatus::Renamed,
|
||||
git2::Status::INDEX_TYPECHANGE => FileStatus::TypeChange,
|
||||
_ => FileStatus::Other,
|
||||
};
|
||||
files.insert(path.to_string(), istatus.to_string());
|
||||
files.insert(path.to_string(), istatus);
|
||||
}
|
||||
|
||||
return Ok(files);
|
||||
|
@ -1,11 +1,8 @@
|
||||
mod activity;
|
||||
mod sessions;
|
||||
mod storage;
|
||||
|
||||
pub use sessions::{id_from_commit, Meta, Session};
|
||||
pub use storage::Store;
|
||||
|
||||
#[cfg(test)]
|
||||
mod activity_tests;
|
||||
#[cfg(test)]
|
||||
mod sessions_tests;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::path::Path;
|
||||
|
||||
use super::activity;
|
||||
use crate::git::activity;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use serde::Serialize;
|
||||
|
||||
|
@ -3,7 +3,7 @@ use std::{
|
||||
time,
|
||||
};
|
||||
|
||||
use crate::{projects, sessions};
|
||||
use crate::{git, projects, sessions};
|
||||
use anyhow::{Context, Result};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -32,9 +32,9 @@ impl Store {
|
||||
let activity = match std::fs::read_to_string(git_repository.path().join("logs/HEAD")) {
|
||||
Ok(reflog) => reflog
|
||||
.lines()
|
||||
.filter_map(|line| sessions::activity::parse_reflog_line(line).ok())
|
||||
.filter_map(|line| git::activity::parse_reflog_line(line).ok())
|
||||
.filter(|activity| activity.timestamp_ms >= now_ts)
|
||||
.collect::<Vec<sessions::activity::Activity>>(),
|
||||
.collect::<Vec<git::activity::Activity>>(),
|
||||
Err(_) => Vec::new(),
|
||||
};
|
||||
|
||||
@ -221,9 +221,9 @@ impl Store {
|
||||
)
|
||||
})?
|
||||
.lines()
|
||||
.filter_map(|line| sessions::activity::parse_reflog_line(line).ok())
|
||||
.filter_map(|line| git::activity::parse_reflog_line(line).ok())
|
||||
.filter(|activity| activity.timestamp_ms >= start_ts)
|
||||
.collect::<Vec<sessions::activity::Activity>>(),
|
||||
.collect::<Vec<git::activity::Activity>>(),
|
||||
false => Vec::new(),
|
||||
};
|
||||
|
||||
|
@ -1,22 +1,29 @@
|
||||
use crate::{events, projects};
|
||||
use crate::{events, repositories};
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn on_git_file_change<P: AsRef<Path>>(
|
||||
sender: &tokio::sync::mpsc::Sender<events::Event>,
|
||||
project: &projects::Project,
|
||||
repository: &repositories::Repository,
|
||||
path: P,
|
||||
) -> Result<()> {
|
||||
if path.as_ref().ne(Path::new(".git/logs/HEAD")) {
|
||||
let event = if path.as_ref().eq(Path::new(".git/logs/HEAD")) {
|
||||
events::Event::git_activity(&repository.project)
|
||||
} else if path.as_ref().eq(Path::new(".git/HEAD")) {
|
||||
events::Event::git_head(&repository.project, &repository.head()?)
|
||||
} else if path.as_ref().eq(Path::new(".git/index")) {
|
||||
events::Event::git_index(&repository.project)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
let event = events::Event::git(&project);
|
||||
};
|
||||
|
||||
sender.send(event).await.with_context(|| {
|
||||
format!(
|
||||
"{}: failed to send git event for \"{}\"",
|
||||
project.id,
|
||||
repository.project.id,
|
||||
path.as_ref().to_str().unwrap()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ impl Watcher {
|
||||
let shared_sender = Arc::new(sender.clone());
|
||||
let shared_deltas_store = Arc::new(repository.deltas_storage.clone());
|
||||
let shared_lock_file = Arc::new(tokio::sync::Mutex::new(lock_file));
|
||||
let shared_repository = Arc::new(repository.clone());
|
||||
|
||||
self.session_watcher
|
||||
.watch(sender, shared_lock_file.clone(), repository)?;
|
||||
@ -62,11 +63,13 @@ impl Watcher {
|
||||
let sender = shared_sender;
|
||||
let deltas_storage = shared_deltas_store;
|
||||
let lock_file = shared_lock_file;
|
||||
let repository = shared_repository;
|
||||
while let Some(event) = fsevents.recv().await {
|
||||
match event {
|
||||
files::Event::FileChange((project, path)) => {
|
||||
if path.starts_with(Path::new(".git")) {
|
||||
if let Err(e) = git::on_git_file_change(&sender, &project, &path).await
|
||||
if let Err(e) =
|
||||
git::on_git_file_change(&sender, &repository, &path).await
|
||||
{
|
||||
log::error!("{}: {:#}", project.id, e);
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
export const toHumanBranchName = (branch: string | undefined) =>
|
||||
branch ? branch.replace('refs/heads/', '') : 'master';
|
@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { toHumanBranchName } from '$lib/branch';
|
||||
|
||||
export let startTime: Date;
|
||||
export let endTime: Date;
|
||||
export let label: string;
|
||||
export let href: string;
|
||||
|
||||
const timeToGridRow = (time: Date) => {
|
||||
const hours = time.getHours();
|
||||
const minutes = time.getMinutes();
|
||||
const totalMinutes = hours * 60 + minutes;
|
||||
const totalMinutesPerDay = 24 * 60;
|
||||
const gridRow = Math.floor((totalMinutes / totalMinutesPerDay) * 96);
|
||||
return gridRow + 1; // offset the first row
|
||||
};
|
||||
|
||||
const dateToGridCol = (date: Date) => {
|
||||
return date.getDay();
|
||||
};
|
||||
|
||||
const timeToSpan = (startTime: Date, endTime: Date) => {
|
||||
const startMinutes = startTime.getHours() * 60 + startTime.getMinutes();
|
||||
const endMinutes = endTime.getHours() * 60 + endTime.getMinutes();
|
||||
const span = Math.round((endMinutes - startMinutes) / 15); // 4 spans per hour
|
||||
if (span < 1) {
|
||||
return 1;
|
||||
} else {
|
||||
return span;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<li
|
||||
class="relative mt-px flex col-start-{dateToGridCol(startTime)}"
|
||||
style="grid-row: {timeToGridRow(startTime)} / span {timeToSpan(startTime, endTime)};"
|
||||
>
|
||||
<a
|
||||
{href}
|
||||
title={startTime.toLocaleTimeString()}
|
||||
class="group absolute inset-1 flex flex-col items-center justify-center rounded-lg bg-zinc-300 p-3 leading-5 shadow hover:bg-zinc-200"
|
||||
>
|
||||
<p class="order-1 font-semibold text-zinc-800">
|
||||
{toHumanBranchName(label)}
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
@ -1 +0,0 @@
|
||||
export { default as WeekBlockEntry } from './WeekBlockEntry.svelte';
|
27
src/lib/git/activity.ts
Normal file
27
src/lib/git/activity.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
export type Activity = {
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const list = (params: { projectId: string; startTimeMs?: number }) =>
|
||||
invoke<Activity[]>('git_activity', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const activity = await list(params);
|
||||
const store = writable(activity);
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/git/activity`, async () => {
|
||||
log.info(`Status: Received git activity event, projectId: ${params.projectId}`);
|
||||
const startTimeMs = activity.at(-1)?.timestampMs;
|
||||
const newActivities = await list({ projectId: params.projectId, startTimeMs });
|
||||
store.update((activities) => [...activities, ...newActivities]);
|
||||
});
|
||||
|
||||
return store as Readable<Activity[]>;
|
||||
};
|
18
src/lib/git/head.ts
Normal file
18
src/lib/git/head.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
const list = (params: { projectId: string }) => invoke<string>('git_head', params);
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const head = await list(params);
|
||||
const store = writable(head);
|
||||
|
||||
appWindow.listen<{ head: string }>(`project://${params.projectId}/git/head`, async (payload) => {
|
||||
log.info(`Status: Received git head event, projectId: ${params.projectId}`);
|
||||
store.set(payload.payload.head);
|
||||
});
|
||||
|
||||
return derived(store, (head) => head.replace('refs/heads/', ''));
|
||||
};
|
2
src/lib/git/index.ts
Normal file
2
src/lib/git/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as statuses } from './statuses';
|
||||
export { default as activity } from './activity';
|
37
src/lib/git/statuses.ts
Normal file
37
src/lib/git/statuses.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
export type Status = {
|
||||
path: string;
|
||||
status: FileStatus;
|
||||
};
|
||||
|
||||
type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'typeChange' | 'other';
|
||||
|
||||
const list = (params: { projectId: string }) =>
|
||||
invoke<Record<string, FileStatus>>('git_status', params);
|
||||
|
||||
const convertToStatuses = (statusesGit: Record<string, FileStatus>): Status[] =>
|
||||
Object.entries(statusesGit).map((status) => ({
|
||||
path: status[0],
|
||||
status: status[1]
|
||||
}));
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const statuses = await list(params).then(convertToStatuses);
|
||||
const store = writable(statuses);
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/git/index`, async () => {
|
||||
log.info(`Status: Received git index event, projectId: ${params.projectId}`);
|
||||
store.set(await list(params).then(convertToStatuses));
|
||||
});
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/sessions`, async () => {
|
||||
log.info(`Status: Received sessions event, projectId: ${params.projectId}`);
|
||||
store.set(await list(params).then(convertToStatuses));
|
||||
});
|
||||
|
||||
return store as Readable<Status[]>;
|
||||
};
|
@ -2,102 +2,97 @@ import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
|
||||
export type Activity = {
|
||||
type: string;
|
||||
timestampMs: number;
|
||||
message: string;
|
||||
};
|
||||
import type { Activity } from './git/activity';
|
||||
|
||||
export namespace Session {
|
||||
export const within = (session: Session | undefined, timestampMs: number) => {
|
||||
if (!session) return false;
|
||||
const { startTimestampMs, lastTimestampMs } = session.meta;
|
||||
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
|
||||
};
|
||||
export const within = (session: Session | undefined, timestampMs: number) => {
|
||||
if (!session) return false;
|
||||
const { startTimestampMs, lastTimestampMs } = session.meta;
|
||||
return startTimestampMs <= timestampMs && timestampMs <= lastTimestampMs;
|
||||
};
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
};
|
||||
activity: Activity[];
|
||||
id: string;
|
||||
hash?: string;
|
||||
meta: {
|
||||
startTimestampMs: number;
|
||||
lastTimestampMs: number;
|
||||
branch?: string;
|
||||
commit?: string;
|
||||
};
|
||||
activity: Activity[];
|
||||
};
|
||||
|
||||
const filesCache: Record<string, Record<string, Promise<Record<string, string>>>> = {};
|
||||
|
||||
export const listFiles = async (params: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
paths?: string[];
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
paths?: string[];
|
||||
}) => {
|
||||
const sessionFilesCache = filesCache[params.projectId] || {};
|
||||
if (params.sessionId in sessionFilesCache) {
|
||||
return sessionFilesCache[params.sessionId].then((files) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(files).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
const sessionFilesCache = filesCache[params.projectId] || {};
|
||||
if (params.sessionId in sessionFilesCache) {
|
||||
return sessionFilesCache[params.sessionId].then((files) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(files).filter(([path]) =>
|
||||
params.paths ? params.paths.includes(path) : true
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const promise = invoke<Record<string, string>>('list_session_files', {
|
||||
sessionId: params.sessionId,
|
||||
projectId: params.projectId
|
||||
});
|
||||
sessionFilesCache[params.sessionId] = promise;
|
||||
filesCache[params.projectId] = sessionFilesCache;
|
||||
return promise.then((files) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(files).filter(([path]) => (params.paths ? params.paths.includes(path) : true))
|
||||
);
|
||||
});
|
||||
const promise = invoke<Record<string, string>>('list_session_files', {
|
||||
sessionId: params.sessionId,
|
||||
projectId: params.projectId
|
||||
});
|
||||
sessionFilesCache[params.sessionId] = promise;
|
||||
filesCache[params.projectId] = sessionFilesCache;
|
||||
return promise.then((files) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(files).filter(([path]) => (params.paths ? params.paths.includes(path) : true))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const sessionsCache: Record<string, Promise<Session[]>> = {};
|
||||
|
||||
const list = async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
if (params.projectId in sessionsCache) {
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
sessions.filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
}
|
||||
sessionsCache[params.projectId] = invoke<Session[]>('list_sessions', {
|
||||
projectId: params.projectId
|
||||
});
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
sessions.filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
if (params.projectId in sessionsCache) {
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
sessions.filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
}
|
||||
sessionsCache[params.projectId] = invoke<Session[]>('list_sessions', {
|
||||
projectId: params.projectId
|
||||
});
|
||||
return sessionsCache[params.projectId].then((sessions) =>
|
||||
sessions.filter((s) =>
|
||||
params.earliestTimestampMs ? s.meta.startTimestampMs >= params.earliestTimestampMs : true
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default async (params: { projectId: string; earliestTimestampMs?: number }) => {
|
||||
const store = writable([] as Session[]);
|
||||
list(params).then((sessions) => {
|
||||
store.set(sessions);
|
||||
});
|
||||
const store = writable([] as Session[]);
|
||||
list(params).then((sessions) => {
|
||||
store.set(sessions);
|
||||
});
|
||||
|
||||
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) => {
|
||||
log.info(`Received sessions event, projectId: ${params.projectId}`);
|
||||
const session = event.payload;
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex((session) => session.id === event.payload.id);
|
||||
if (index === -1) {
|
||||
return [...sessions, session];
|
||||
} else {
|
||||
return [...sessions.slice(0, index), session, ...sessions.slice(index + 1)];
|
||||
}
|
||||
});
|
||||
});
|
||||
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) => {
|
||||
log.info(`Received sessions event, projectId: ${params.projectId}`);
|
||||
const session = event.payload;
|
||||
store.update((sessions) => {
|
||||
const index = sessions.findIndex((session) => session.id === event.payload.id);
|
||||
if (index === -1) {
|
||||
return [...sessions, session];
|
||||
} else {
|
||||
return [...sessions.slice(0, index), session, ...sessions.slice(index + 1)];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return store as Readable<Session[]>;
|
||||
return store as Readable<Session[]>;
|
||||
};
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { writable, type Readable } from 'svelte/store';
|
||||
import { log } from '$lib';
|
||||
import type { Session } from '$lib/sessions';
|
||||
|
||||
export type Status = {
|
||||
path: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const listFiles = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_status', params);
|
||||
|
||||
function convertToStatuses(statusesGit: Record<string, string>): Status[] {
|
||||
return Object.entries(statusesGit).map((status) => {
|
||||
return {
|
||||
path: status[0],
|
||||
status: status[1]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default async (params: { projectId: string }) => {
|
||||
const statuses: Status[] = [];
|
||||
listFiles(params).then((statuses) => {
|
||||
store.set(convertToStatuses(statuses));
|
||||
});
|
||||
|
||||
const store = writable(statuses);
|
||||
|
||||
appWindow.listen(`project://${params.projectId}/git`, async (event) => {
|
||||
log.info(`Status: Received git event, projectId: ${params.projectId}`);
|
||||
const statusesGit = await listFiles(params);
|
||||
const statuses = convertToStatuses(statusesGit);
|
||||
store.set(statuses);
|
||||
});
|
||||
|
||||
appWindow.listen<Session>(`project://${params.projectId}/sessions`, async (event) => {
|
||||
log.info(`Status: Received sessions event, projectId: ${params.projectId}`);
|
||||
const statusesGit = await listFiles(params);
|
||||
const statuses = convertToStatuses(statusesGit);
|
||||
store.set(statuses);
|
||||
});
|
||||
|
||||
return store as Readable<Status[]>;
|
||||
};
|
@ -1,60 +1,26 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { building } from '$app/environment';
|
||||
import { readable, derived } from 'svelte/store';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import type { Status } from '$lib/statuses';
|
||||
import type { Activity } from '$lib/sessions';
|
||||
import { subDays, getTime } from 'date-fns';
|
||||
import type { Status } from '$lib/git/statuses';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const prerender = false;
|
||||
export const load: LayoutLoad = async ({ parent, params }) => {
|
||||
const { projects } = await parent();
|
||||
|
||||
const filesStatus = building
|
||||
? readable<Status[]>([])
|
||||
: await (await import('$lib/statuses')).default({ projectId: params.projectId });
|
||||
|
||||
const sessionsFromLastFourDays = building
|
||||
? readable<Session[]>([])
|
||||
: await (
|
||||
await import('$lib/sessions')
|
||||
).default({
|
||||
projectId: params.projectId,
|
||||
earliestTimestampMs: getTime(subDays(new Date(), 4))
|
||||
});
|
||||
const orderedSessionsFromLastFourDays = derived(sessionsFromLastFourDays, (sessions) => {
|
||||
return sessions.slice().sort((a, b) => a.meta.startTimestampMs - b.meta.startTimestampMs);
|
||||
});
|
||||
const recentActivity = derived(sessionsFromLastFourDays, (sessions) => {
|
||||
const recentActivity: Activity[] = [];
|
||||
sessions.forEach((session) => {
|
||||
session.activity.forEach((activity) => {
|
||||
recentActivity.push(activity);
|
||||
});
|
||||
});
|
||||
const activitySorted = recentActivity.sort((a, b) => {
|
||||
return b.timestampMs - a.timestampMs;
|
||||
});
|
||||
return activitySorted.slice(0, 20);
|
||||
});
|
||||
const user = building
|
||||
? {
|
||||
...readable<undefined>(undefined),
|
||||
set: () => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
delete: () => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/users')).default();
|
||||
|
||||
return {
|
||||
user: user,
|
||||
project: projects.get(params.projectId),
|
||||
projectId: params.projectId,
|
||||
orderedSessionsFromLastFourDays: orderedSessionsFromLastFourDays,
|
||||
filesStatus: filesStatus,
|
||||
recentActivity: recentActivity
|
||||
};
|
||||
const { projects } = await parent();
|
||||
const sessions = building
|
||||
? readable<Session[]>([])
|
||||
: await import('$lib/sessions').then((m) => m.default({ projectId: params.projectId }));
|
||||
const statuses = building
|
||||
? readable<Status[]>([])
|
||||
: await import('$lib/git/statuses').then((m) => m.default({ projectId: params.projectId }));
|
||||
const head = building
|
||||
? readable<string>('')
|
||||
: await import('$lib/git/head').then((m) => m.default({ projectId: params.projectId }));
|
||||
return {
|
||||
head,
|
||||
statuses,
|
||||
sessions,
|
||||
project: projects.get(params.projectId),
|
||||
projectId: params.projectId
|
||||
};
|
||||
};
|
||||
|
@ -1,19 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { LayoutData } from './$types';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import { format, startOfDay } from 'date-fns';
|
||||
import { format, getTime, startOfDay, subDays } from 'date-fns';
|
||||
import type { Delta } from '$lib/deltas';
|
||||
import { collapsable } from '$lib/paths';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { toHumanBranchName } from '$lib/branch';
|
||||
import { list as listDeltas } from '$lib/deltas';
|
||||
const getBranch = (params: { projectId: string }) => invoke<string>('git_branch', params);
|
||||
import type { PageData } from './$types';
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export let data: LayoutData;
|
||||
$: project = data.project;
|
||||
$: filesStatus = data.filesStatus;
|
||||
$: recentActivity = data.recentActivity;
|
||||
$: orderedSessionsFromLastFourDays = data.orderedSessionsFromLastFourDays;
|
||||
export let data: PageData;
|
||||
const { activity, project, statuses, sessions, head } = data;
|
||||
|
||||
let latestDeltasByDateByFile: Record<number, Record<string, Delta[][]>[]> = {};
|
||||
|
||||
@ -29,10 +24,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
const recentSessions = derived(sessions, (sessions) => {
|
||||
const lastFourDaysOfSessions = sessions.filter(
|
||||
(session) => session.meta.startTimestampMs >= getTime(subDays(new Date(), 4))
|
||||
);
|
||||
if (lastFourDaysOfSessions.length >= 4) return lastFourDaysOfSessions;
|
||||
return sessions.slice(0, 4);
|
||||
});
|
||||
|
||||
const recentActivity = derived([activity, recentSessions], ([activity, recentSessions]) =>
|
||||
activity
|
||||
.filter((a) => a.timestampMs >= (recentSessions.at(-1)?.meta.startTimestampMs ?? 0))
|
||||
.sort((a, b) => b.timestampMs - a.timestampMs)
|
||||
);
|
||||
|
||||
$: if ($project) {
|
||||
latestDeltasByDateByFile = {};
|
||||
const dateSessions: Record<number, Session[]> = {};
|
||||
$orderedSessionsFromLastFourDays.forEach((session) => {
|
||||
$recentSessions.forEach((session) => {
|
||||
const date = startOfDay(new Date(session.meta.startTimestampMs));
|
||||
if (dateSessions[date.getTime()]) {
|
||||
dateSessions[date.getTime()]?.push(session);
|
||||
@ -74,13 +83,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
let gitBranch = <string | undefined>undefined;
|
||||
$: if ($project) {
|
||||
getBranch({ projectId: $project?.id }).then((branch) => {
|
||||
gitBranch = branch;
|
||||
});
|
||||
}
|
||||
|
||||
// convert a list of timestamps to a sparkline
|
||||
function timestampsToSpark(tsArray: number[]) {
|
||||
let range = tsArray[0] - tsArray[tsArray.length - 1];
|
||||
@ -223,52 +225,47 @@
|
||||
>
|
||||
<div class="work-in-progress-container border-b border-zinc-700 py-4 px-4 ">
|
||||
<h2 class="mb-2 text-lg font-bold text-zinc-300">Work in Progress</h2>
|
||||
{#if gitBranch}
|
||||
<div class="w-100 mb-4 flex items-center justify-between">
|
||||
<div
|
||||
class="button group flex max-w-[200px] justify-between rounded border border-zinc-600 bg-zinc-700 py-2 px-4 text-zinc-300 shadow"
|
||||
>
|
||||
<div class="h-4 w-4">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
width="16"
|
||||
data-view-component="true"
|
||||
class="h-4 w-4 fill-zinc-400"
|
||||
>
|
||||
<path
|
||||
d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
title={toHumanBranchName(gitBranch)}
|
||||
class="truncate pl-2 font-mono text-zinc-300"
|
||||
<div class="w-100 mb-4 flex items-center justify-between">
|
||||
<div
|
||||
class="button group flex max-w-[200px] justify-between rounded border border-zinc-600 bg-zinc-700 py-2 px-4 text-zinc-300 shadow"
|
||||
>
|
||||
<div class="h-4 w-4">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
width="16"
|
||||
data-view-component="true"
|
||||
class="h-4 w-4 fill-zinc-400"
|
||||
>
|
||||
{toHumanBranchName(gitBranch)}
|
||||
</div>
|
||||
<div class="carrot flex hidden items-center pl-3">
|
||||
<svg width="7" height="5" viewBox="0 0 7 5" fill="none" class="fill-zinc-400">
|
||||
<path
|
||||
d="M3.87796 4.56356C3.67858 4.79379 3.32142 4.79379 3.12204 4.56356L0.319371 1.32733C0.0389327 1.00351 0.268959 0.5 0.697336 0.5L6.30267 0.500001C6.73104 0.500001 6.96107 1.00351 6.68063 1.32733L3.87796 4.56356Z"
|
||||
fill="#A1A1AA"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<path
|
||||
d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="/projects/{$project?.id}/commit"
|
||||
title="Commit changes"
|
||||
class="button rounded bg-blue-600 py-2 px-3 text-white hover:bg-blue-700"
|
||||
>Commit changes</a
|
||||
>
|
||||
<div title={$head} class="truncate pl-2 font-mono text-zinc-300">
|
||||
{$head}
|
||||
</div>
|
||||
<div class="carrot flex hidden items-center pl-3">
|
||||
<svg width="7" height="5" viewBox="0 0 7 5" fill="none" class="fill-zinc-400">
|
||||
<path
|
||||
d="M3.87796 4.56356C3.67858 4.79379 3.32142 4.79379 3.12204 4.56356L0.319371 1.32733C0.0389327 1.00351 0.268959 0.5 0.697336 0.5L6.30267 0.500001C6.73104 0.500001 6.96107 1.00351 6.68063 1.32733L3.87796 4.56356Z"
|
||||
fill="#A1A1AA"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $filesStatus.length == 0}
|
||||
<div>
|
||||
<a
|
||||
href="/projects/{$project?.id}/commit"
|
||||
title="Commit changes"
|
||||
class="button rounded bg-blue-600 py-2 px-3 text-white hover:bg-blue-700"
|
||||
>Commit changes</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{#if $statuses.length == 0}
|
||||
<div
|
||||
class="flex rounded border border-green-700 bg-green-900 p-4 align-middle text-green-400"
|
||||
>
|
||||
@ -284,21 +281,14 @@
|
||||
Everything is committed
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded border border-yellow-400 bg-yellow-500 p-4 font-mono text-yellow-900">
|
||||
<ul class="pl-4">
|
||||
{#each $filesStatus as activity}
|
||||
<li class="list-disc">
|
||||
<div class="flex w-full gap-2 ">
|
||||
{activity.status.slice(0, 1)}
|
||||
<div
|
||||
class="truncate"
|
||||
use:collapsable={{ value: activity.path, separator: '/' }}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<ul class="rounded border border-yellow-400 bg-yellow-500 p-4 font-mono text-yellow-900">
|
||||
{#each $statuses as activity}
|
||||
<li class="flex w-full gap-2">
|
||||
<span class="font-semibold">{activity.status.slice(0, 1).toUpperCase()}</span>
|
||||
<span class="truncate" use:collapsable={{ value: activity.path, separator: '/' }} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
|
@ -1,20 +1,13 @@
|
||||
import { building } from '$app/environment';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { PageLoad } from './$types';
|
||||
import { readable } from 'svelte/store';
|
||||
import type { Activity } from '$lib/git/activity';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const user = building
|
||||
? {
|
||||
...readable<undefined>(undefined),
|
||||
set: () => {
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
delete: () => {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
}
|
||||
: await (await import('$lib/users')).default();
|
||||
return {
|
||||
user
|
||||
};
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
const activity = building
|
||||
? readable<Activity[]>([])
|
||||
: await import('$lib/git/activity').then((m) => m.default({ projectId: params.projectId }));
|
||||
return {
|
||||
activity
|
||||
};
|
||||
};
|
||||
|
@ -5,13 +5,12 @@
|
||||
import { collapsable } from '$lib/paths';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { toHumanBranchName } from '$lib/branch';
|
||||
import DiffViewer from '$lib/components/DiffViewer.svelte';
|
||||
|
||||
const api = Api({ fetch });
|
||||
|
||||
export let data: PageData;
|
||||
const { project, user, filesStatus } = data;
|
||||
const { project, user, statuses, head } = data;
|
||||
|
||||
let commitSubject: string;
|
||||
let placeholderSubject = 'Summary (required)';
|
||||
@ -37,7 +36,7 @@
|
||||
message: commitSubject,
|
||||
files: filesSelectedForCommit,
|
||||
push: false
|
||||
}).then((result) => {
|
||||
}).then(() => {
|
||||
toast.success('Commit successful!', {
|
||||
icon: '🎉'
|
||||
});
|
||||
@ -55,7 +54,7 @@
|
||||
filesSelectedForCommit = [];
|
||||
};
|
||||
const toggleAllOn = () => {
|
||||
filesSelectedForCommit = $filesStatus.map((file) => {
|
||||
filesSelectedForCommit = $statuses.map((file) => {
|
||||
return file.path;
|
||||
});
|
||||
};
|
||||
@ -65,11 +64,9 @@
|
||||
|
||||
const getDiff = (params: { projectId: string }) =>
|
||||
invoke<Record<string, string>>('git_wd_diff', params);
|
||||
const getBranch = (params: { projectId: string }) => invoke<string>('git_branch', params);
|
||||
const getFile = (params: { projectId: string; path: string }) =>
|
||||
invoke<string>('get_file_contents', params);
|
||||
|
||||
let gitBranch: string | undefined = undefined;
|
||||
let gitDiff: Record<string, string> = {};
|
||||
let generatedMessage: string | undefined = undefined;
|
||||
let isLoaded = false;
|
||||
@ -88,7 +85,7 @@
|
||||
currentPath = path;
|
||||
currentDiff = gitDiff[path];
|
||||
} else {
|
||||
let file = $filesStatus.filter((file) => file.path === path)[0];
|
||||
let file = $statuses.filter((file) => file.path === path)[0];
|
||||
if ($project && file) {
|
||||
fileContentsStatus = file.status;
|
||||
getFile({ projectId: $project.id, path: path }).then((contents) => {
|
||||
@ -101,12 +98,6 @@
|
||||
|
||||
$: if ($project) {
|
||||
if (!isLoaded) {
|
||||
getBranch({ projectId: $project?.id }).then((branch) => {
|
||||
gitBranch = branch;
|
||||
filesSelectedForCommit = $filesStatus.map((file) => {
|
||||
return file.path;
|
||||
});
|
||||
});
|
||||
getDiff({ projectId: $project?.id }).then((diff) => {
|
||||
gitDiff = diff;
|
||||
});
|
||||
@ -176,7 +167,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="truncate pl-2 font-mono text-zinc-300">
|
||||
{toHumanBranchName(gitBranch)}
|
||||
{$head}
|
||||
</div>
|
||||
<div class="carrot flex hidden items-center pl-3">
|
||||
<svg width="7" height="5" viewBox="0 0 7 5" fill="none" class="fill-zinc-400">
|
||||
@ -204,7 +195,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ul class="min-h-[35px] truncate px-2 py-2">
|
||||
{#each $filesStatus as activity}
|
||||
{#each $statuses as activity}
|
||||
<li class="list-none text-zinc-300">
|
||||
<div class="flex flex-row align-middle">
|
||||
<input
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { building } from '$app/environment';
|
||||
import type { Session } from '$lib/sessions';
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async ({ params }) => {
|
||||
const sessions: Readable<Session[]> = building
|
||||
? readable<Session[]>([])
|
||||
: await import('$lib/sessions').then((m) => m.default({ projectId: params.projectId }));
|
||||
return {
|
||||
sessions,
|
||||
projectId: params.projectId
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user