Skip to content

Commit 6b3c4a9

Browse files
committed
feat(import-export): add settings and excluded dependencies to the export
1 parent 0e4c5ae commit 6b3c4a9

File tree

9 files changed

+129
-78
lines changed

9 files changed

+129
-78
lines changed

src/assets/global.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,8 @@ dialog {
260260
&.invalid {
261261
border-color: var(--negative);
262262
}
263-
> * {
264-
padding: 1.5rem;
263+
> *:not(header) {
264+
margin: 1.5rem;
265265
}
266266
> header {
267267
display: flex;

src/components/header/import-export.vue

Lines changed: 0 additions & 52 deletions
This file was deleted.

src/components/header/main-header.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@
1818
Update app
1919
</button>
2020
</div>
21-
<div>
22-
<import-export :no-data="isEmpty" />
23-
<add-repo class="main-header__block-button" />
24-
</div>
21+
<add-repo class="main-header__block-button" />
2522
</header>
2623
</template>
2724
<script setup lang="ts">
@@ -37,7 +34,6 @@ import AboutModal from "../modals/about-modal.vue";
3734
import AddRepo from "../modals/add-repo.vue";
3835
import SettingsModal from "../modals/settings-modal.vue";
3936
import HeaderSummary from "./header-summary.vue";
40-
import ImportExport from "./import-export.vue";
4137
4238
const { isEmpty, updateRepositories } = useRepositoriesStore();
4339
const { updateLatestVersions } = useLatestVersionsStore();

src/components/import-export.vue

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<template>
2+
<section class="import-export">
3+
<button title="export repositories" type="button" @click="exportSettings">
4+
<icon-download />
5+
Export settings
6+
</button>
7+
<button title="import repositories" type="button" @click="importFile()">
8+
<icon-upload v-if="!isImporting" />
9+
<icon-loader v-else :model-value="true" />
10+
Import settings
11+
</button>
12+
</section>
13+
</template>
14+
<script setup lang="ts">
15+
import { ref } from "vue";
16+
import { useFileDialog, whenever } from "@vueuse/core";
17+
import { IconDownload, IconUpload } from "@tabler/icons-vue";
18+
import dayjs from "dayjs";
19+
import { isExportedData, type ExportedData } from "@/helpers/export";
20+
import { downloadFile, readFile } from "@/helpers/file";
21+
import { isValidJSON } from "@/helpers/validate";
22+
import { useExcludedDependenciesStore } from "@/store/excluded-dependencies";
23+
import { useRepositoriesStore } from "@/store/repositories";
24+
import { useSettingsStore } from "@/store/settings";
25+
import IconLoader from "./icon-loader.vue";
26+
27+
const { importRepositories, exportRepositories } = useRepositoriesStore();
28+
const { settings, importSettings } = useSettingsStore();
29+
const { excludedDependencies, importExcludedDependencies } = useExcludedDependenciesStore();
30+
31+
// Import
32+
const isImporting = ref<boolean>(false);
33+
const { files, open: importFile } = useFileDialog({ multiple: false, reset: true, accept: "application/json" });
34+
whenever(files, async ({ 0: payload }) => {
35+
try {
36+
isImporting.value = true;
37+
38+
const content = await readFile(payload).then(String);
39+
if (!isValidJSON(content)) throw new Error("Invalid JSON");
40+
const parsedData = JSON.parse(content);
41+
if (!isExportedData(parsedData)) throw new Error("Invalid import data");
42+
43+
importSettings(parsedData.settings);
44+
if (parsedData.excludedDependencies.length) { importExcludedDependencies(parsedData.excludedDependencies); }
45+
if (parsedData.repositories.length) { await importRepositories(parsedData.repositories); }
46+
} finally {
47+
isImporting.value = false;
48+
}
49+
});
50+
51+
// Export
52+
function exportSettings(): void {
53+
const fileName = `github-metrics-${dayjs().format("YYYY-MM-DD")}.json`;
54+
const payload: ExportedData = {
55+
settings: settings.value,
56+
excludedDependencies: [...excludedDependencies.value.values()],
57+
repositories: exportRepositories()
58+
};
59+
downloadFile(JSON.stringify(payload), fileName, "application/json");
60+
}
61+
</script>
62+
<style lang="scss">
63+
.import-export {
64+
display: grid;
65+
grid-template-columns: repeat(2, 1fr);
66+
gap: 1rem;
67+
@media (width <= 600px) {
68+
grid-template-columns: 1fr;
69+
}
70+
}
71+
</style>

src/components/modals/settings-modal.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<icon-x />
1212
</button>
1313
</header>
14+
<import-export />
1415
<form class="settings__form" @submit.prevent="update">
1516
<fieldset>
1617
<legend>Interface</legend>
@@ -66,6 +67,7 @@ import { useDialog } from "@/composable/useDialog";
6667
import { deepCopy } from "@/helpers/object";
6768
import { fetchCurrentUser, setAuthToken } from "@/service/octokit";
6869
import { useSettingsStore } from "@/store/settings";
70+
import ImportExport from "@/components/import-export.vue";
6971
import InputSelect from "@/components/input-select.vue";
7072
7173
const themes = [
@@ -80,6 +82,7 @@ const { settings } = useSettingsStore();
8082
const dialogRef = useTemplateRef("dialogElement");
8183
const { open, close } = useDialog(dialogRef);
8284
const form = reactive(deepCopy(settings.value));
85+
watch(settings, (value) => { Object.assign(form, value); }, { deep: true });
8386
8487
async function getUsername(): Promise<void> {
8588
const user = await fetchCurrentUser();

src/helpers/export.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { isObject } from "@vueuse/core";
2+
import type { Repository } from "@/composable/useRepo";
3+
import type { SettingsStore } from "@/store/settings";
4+
5+
export interface ExportedData {
6+
settings: SettingsStore
7+
excludedDependencies: string[]
8+
repositories: ExportedRepository[]
9+
}
10+
11+
export function isExportedData(data: unknown): data is ExportedData {
12+
return isObject(data) && "settings" in data && "excludedDependencies" in data && "repositories" in data;
13+
}
14+
15+
export type ExportedRepository = Pick<Repository, "id" | "full_name" | "integrations">;
16+
export function isExportedRepository(data: unknown): data is ExportedRepository {
17+
return isObject(data) && "id" in data && "full_name" in data && "integrations" in data;
18+
}

src/store/excluded-dependencies.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ export const useExcludedDependenciesStore = createSharedComposable(() => {
1313
excludedDependencies.value.delete(dep);
1414
}
1515

16+
function importExcludedDependencies(deps: string[]): void {
17+
excludedDependencies.value = new Set(deps);
18+
}
19+
1620
return {
1721
excludedDependencies,
22+
importExcludedDependencies,
1823
hideDependency,
1924
showDependency
2025
};

src/store/repositories.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { computed } from "vue";
2-
import { createSharedComposable, isObject, useLocalStorage } from "@vueuse/core";
2+
import { createSharedComposable, useLocalStorage } from "@vueuse/core";
33
import dayjs from "dayjs";
4+
import { isExportedRepository, type ExportedRepository } from "@/helpers/export";
45
import { fetchRepo, fetchRepositoryFiles, fetchRepositoryPackages, fetchRepositoryWorkflows } from "@/service/octokit";
56
import type { Repository } from "@/composable/useRepo";
67

@@ -9,11 +10,6 @@ interface RepositoriesStore {
910
data: Repository[]
1011
};
1112

12-
export type ExportedRepository = Pick<Repository, "id" | "full_name" | "integrations">;
13-
export function isExportedRepository(data: unknown): data is ExportedRepository {
14-
return isObject(data) && "id" in data && "full_name" in data && "integrations" in data;
15-
}
16-
1713
const DEFAULT_STORE: RepositoriesStore = {
1814
lastUpdate: dayjs().toISOString(),
1915
data: []
@@ -49,7 +45,10 @@ export const useRepositoriesStore = createSharedComposable(() => {
4945
return repositories.value.some(({ id: repoId }) => repoId === id);
5046
}
5147

52-
async function addRepository(fullName: Repository["full_name"], integrations: Repository["integrations"] = {}): Promise<void> {
48+
async function addRepository(
49+
fullName: Repository["full_name"],
50+
integrations: Repository["integrations"] = {}
51+
): Promise<void> {
5352
const repo = await fetchRepo(fullName);
5453
if (!repo) throw new Error("Repo not found");
5554
if (isRepoExists(repo.id)) return updateRepository(fullName, integrations);
@@ -65,7 +64,10 @@ export const useRepositoriesStore = createSharedComposable(() => {
6564
repositories.value = repositories.value.filter((repo) => repo.id !== id);
6665
}
6766

68-
async function updateRepository(fullName: Repository["full_name"], integrations: Repository["integrations"]): Promise<void> {
67+
async function updateRepository(
68+
fullName: Repository["full_name"],
69+
integrations: Repository["integrations"]
70+
): Promise<void> {
6971
const repo = await fetchRepo(fullName);
7072
if (!repo) throw new Error("Repo not found");
7173

@@ -85,17 +87,20 @@ export const useRepositoriesStore = createSharedComposable(() => {
8587
}
8688

8789
async function importRepositories(payload: ExportedRepository[]): Promise<void> {
88-
const fetchPromises = payload.map(({ id, full_name, integrations }) => isRepoExists(id) ?
89-
updateRepository(full_name, integrations) :
90-
addRepository(full_name, integrations)
91-
);
90+
const fetchPromises = payload.reduce((acc: Promise<void>[], repo) => {
91+
if (isExportedRepository(repo)) {
92+
const { id, full_name, integrations } = repo;
93+
const request = isRepoExists(id) ?
94+
updateRepository(full_name, integrations) :
95+
addRepository(full_name, integrations);
96+
acc.push(request);
97+
}
98+
return acc;
99+
}, []);
92100
await Promise.all(fetchPromises);
93101
}
94-
function exportRepositories(): string {
95-
const repos: ExportedRepository[] = repositories.value.map(
96-
({ id, full_name, integrations }) => ({ id, full_name, integrations })
97-
);
98-
return JSON.stringify(repos, null, 2);
102+
function exportRepositories(): ExportedRepository[] {
103+
return repositories.value.map(({ id, full_name, integrations }) => ({ id, full_name, integrations }));
99104
}
100105

101106
function updateCheck() {

src/store/settings.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createSharedComposable, useLocalStorage } from "@vueuse/core";
22
import { useRegisterSW } from "virtual:pwa-register/vue";
33

44
type Theme = "github" | "blue" | "beige" | "green" | "red";
5-
interface SettingsStore {
5+
export interface SettingsStore {
66
authToken: string
77
username: string
88
displayOwner: boolean
@@ -23,9 +23,14 @@ export const useSettingsStore = createSharedComposable(() => {
2323

2424
const { needRefresh, updateServiceWorker } = useRegisterSW({ immediate: true });
2525

26+
function importSettings(newSettings: Partial<SettingsStore>): void {
27+
Object.assign(settings.value, newSettings);
28+
}
29+
2630
return {
2731
settings,
2832
needRefresh,
29-
updateServiceWorker
33+
updateServiceWorker,
34+
importSettings
3035
};
3136
});

0 commit comments

Comments
 (0)