mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-26 03:41:01 +00:00
Merge branch 'main' into feat/project-settings
This commit is contained in:
commit
69c1d04016
29 changed files with 289 additions and 158 deletions
|
@ -41,9 +41,6 @@ variables:
|
||||||
|
|
||||||
when:
|
when:
|
||||||
- event: [pull_request, tag]
|
- event: [pull_request, tag]
|
||||||
- event: push
|
|
||||||
branch:
|
|
||||||
- renovate/*
|
|
||||||
- event: push
|
- event: push
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
path: *when_path
|
path: *when_path
|
||||||
|
|
|
@ -31,7 +31,6 @@ when:
|
||||||
- <<: *docker_path
|
- <<: *docker_path
|
||||||
branch:
|
branch:
|
||||||
- ${CI_REPO_DEFAULT_BRANCH}
|
- ${CI_REPO_DEFAULT_BRANCH}
|
||||||
- renovate/*
|
|
||||||
- event: pull_request_closed
|
- event: pull_request_closed
|
||||||
path: *when_path
|
path: *when_path
|
||||||
- event: manual
|
- event: manual
|
||||||
|
|
|
@ -3,7 +3,6 @@ when:
|
||||||
- event: push
|
- event: push
|
||||||
branch:
|
branch:
|
||||||
- ${CI_REPO_DEFAULT_BRANCH}
|
- ${CI_REPO_DEFAULT_BRANCH}
|
||||||
- renovate/*
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.2.0
|
- &trivy_plugin docker.io/woodpeckerci/plugin-trivy:1.2.0
|
||||||
|
|
|
@ -9,8 +9,6 @@ steps:
|
||||||
depends_on: []
|
depends_on: []
|
||||||
when:
|
when:
|
||||||
- event: pull_request
|
- event: pull_request
|
||||||
- event: push
|
|
||||||
branch: renovate/*
|
|
||||||
|
|
||||||
- name: spellcheck
|
- name: spellcheck
|
||||||
image: docker.io/node:23-alpine
|
image: docker.io/node:23-alpine
|
||||||
|
|
|
@ -16,8 +16,6 @@ variables:
|
||||||
|
|
||||||
when:
|
when:
|
||||||
- event: pull_request
|
- event: pull_request
|
||||||
- event: push
|
|
||||||
branch: renovate/*
|
|
||||||
- event: push
|
- event: push
|
||||||
branch: ${CI_REPO_DEFAULT_BRANCH}
|
branch: ${CI_REPO_DEFAULT_BRANCH}
|
||||||
path: *when_path
|
path: *when_path
|
||||||
|
|
|
@ -3,7 +3,6 @@ when:
|
||||||
- event: push
|
- event: push
|
||||||
branch:
|
branch:
|
||||||
- release/*
|
- release/*
|
||||||
- renovate/*
|
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &node_image 'docker.io/node:23-alpine'
|
- &node_image 'docker.io/node:23-alpine'
|
||||||
|
|
|
@ -34,7 +34,7 @@ Install make on:
|
||||||
|
|
||||||
### Install Node.js & `pnpm`
|
### Install Node.js & `pnpm`
|
||||||
|
|
||||||
Install [Node.js (>=14)](https://nodejs.org/en/download/) if you want to build Woodpecker's UI or documentation.
|
Install [Node.js (>=20)](https://nodejs.org/en/download/package-manager) if you want to build Woodpecker's UI or documentation.
|
||||||
|
|
||||||
For dependency installation (`node_modules`) of UI and documentation of Woodpecker the package manager pnpm is used.
|
For dependency installation (`node_modules`) of UI and documentation of Woodpecker the package manager pnpm is used.
|
||||||
[This guide](https://pnpm.io/installation) describes the installation of `pnpm`.
|
[This guide](https://pnpm.io/installation) describes the installation of `pnpm`.
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
|
|
|
@ -67,6 +67,21 @@
|
||||||
"success": "Repository enabled"
|
"success": "Repository enabled"
|
||||||
},
|
},
|
||||||
"open_in_forge": "Open repository in forge",
|
"open_in_forge": "Open repository in forge",
|
||||||
|
"visibility": {
|
||||||
|
"visibility": "Project visibility",
|
||||||
|
"public": {
|
||||||
|
"public": "Public",
|
||||||
|
"desc": "Every user can see your project without being logged in."
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"private": "Private",
|
||||||
|
"desc": "Only you and other owners of the repository can see this project."
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"internal": "Internal",
|
||||||
|
"desc": "Only authenticated users of the Woodpecker instance can see this project."
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"not_allowed": "You are not allowed to access this repository's settings",
|
"not_allowed": "You are not allowed to access this repository's settings",
|
||||||
"general": {
|
"general": {
|
||||||
|
@ -107,21 +122,6 @@
|
||||||
"desc": "Pipeline containers get access to security privileges."
|
"desc": "Pipeline containers get access to security privileges."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"visibility": {
|
|
||||||
"visibility": "Project visibility",
|
|
||||||
"public": {
|
|
||||||
"public": "Public",
|
|
||||||
"desc": "Every user can see your project without being logged in."
|
|
||||||
},
|
|
||||||
"private": {
|
|
||||||
"private": "Private",
|
|
||||||
"desc": "Only you and other owners of the repository can see this project."
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"internal": "Internal",
|
|
||||||
"desc": "Only authenticated users of the Woodpecker instance can see this project."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"timeout": "Timeout",
|
"timeout": "Timeout",
|
||||||
"minutes": "minutes"
|
"minutes": "minutes"
|
||||||
|
@ -191,7 +191,7 @@
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"config": "Config",
|
"config": "Config",
|
||||||
"files": "Changed files ({files})",
|
"files": "Changed files",
|
||||||
"no_pipelines": "No pipelines have been started yet.",
|
"no_pipelines": "No pipelines have been started yet.",
|
||||||
"no_pipeline_steps": "No pipeline steps available!",
|
"no_pipeline_steps": "No pipeline steps available!",
|
||||||
"step_not_started": "This step hasn't started yet.",
|
"step_not_started": "This step hasn't started yet.",
|
||||||
|
@ -248,8 +248,8 @@
|
||||||
"failure": "failure",
|
"failure": "failure",
|
||||||
"killed": "killed"
|
"killed": "killed"
|
||||||
},
|
},
|
||||||
"errors": "Errors ({count})",
|
"errors": "Errors",
|
||||||
"warnings": "Warnings ({count})",
|
"warnings": "Warnings",
|
||||||
"show_errors": "Show errors",
|
"show_errors": "Show errors",
|
||||||
"we_got_some_errors": "Oh no, we got some errors!",
|
"we_got_some_errors": "Oh no, we got some errors!",
|
||||||
"duration": "Pipeline duration",
|
"duration": "Pipeline duration",
|
||||||
|
@ -310,7 +310,7 @@
|
||||||
"placeholder": "Stop agent from taking new tasks"
|
"placeholder": "Stop agent from taking new tasks"
|
||||||
},
|
},
|
||||||
"token": "Token",
|
"token": "Token",
|
||||||
"platform": {
|
"platform": {gg
|
||||||
"platform": "Platform",
|
"platform": "Platform",
|
||||||
"badge": "platform"
|
"badge": "platform"
|
||||||
},
|
},
|
||||||
|
@ -508,6 +508,8 @@
|
||||||
"none_desc": "Every event triggers pipelines, including pull requests. This setting can be dangerous and is only recommended for private instances.",
|
"none_desc": "Every event triggers pipelines, including pull requests. This setting can be dangerous and is only recommended for private instances.",
|
||||||
"forks": "Pull request from forked repository",
|
"forks": "Pull request from forked repository",
|
||||||
"pull_requests": "All pull requests",
|
"pull_requests": "All pull requests",
|
||||||
"all_events": "Any event trigger"
|
"all_events": "All events from forge"
|
||||||
}
|
},
|
||||||
|
"all_repositories": "All repositories",
|
||||||
|
"no_search_results": "No results found"
|
||||||
}
|
}
|
||||||
|
|
13
web/src/components/atomic/CountBadge.vue
Normal file
13
web/src/components/atomic/CountBadge.vue
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="text-xs font-bold inline-block min-w-5 leading-4 rounded-full bg-wp-background-300 dark:bg-wp-background-100 text-wp-text-100 text-center py-0.5 px-1.5"
|
||||||
|
>
|
||||||
|
{{ value }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
value?: string | number;
|
||||||
|
}>();
|
||||||
|
</script>
|
|
@ -13,45 +13,28 @@
|
||||||
</InputField>
|
</InputField>
|
||||||
<InputField v-slot="{ id }" :label="$t('repo.deploy_pipeline.variables.title')">
|
<InputField v-slot="{ id }" :label="$t('repo.deploy_pipeline.variables.title')">
|
||||||
<span class="text-sm text-wp-text-alt-100 mb-2">{{ $t('repo.deploy_pipeline.variables.desc') }}</span>
|
<span class="text-sm text-wp-text-alt-100 mb-2">{{ $t('repo.deploy_pipeline.variables.desc') }}</span>
|
||||||
<div class="flex flex-col gap-2">
|
<KeyValueEditor
|
||||||
<div v-for="(_, i) in payload.variables" :key="i" class="flex gap-4">
|
:id="id"
|
||||||
<TextField
|
v-model="payload.variables"
|
||||||
:id="id"
|
:key-placeholder="$t('repo.deploy_pipeline.variables.name')"
|
||||||
v-model="payload.variables[i].name"
|
:value-placeholder="$t('repo.deploy_pipeline.variables.value')"
|
||||||
:placeholder="$t('repo.deploy_pipeline.variables.name')"
|
:delete-title="$t('repo.deploy_pipeline.variables.delete')"
|
||||||
/>
|
@update:is-valid="isVariablesValid = $event"
|
||||||
<TextField
|
/>
|
||||||
:id="id"
|
|
||||||
v-model="payload.variables[i].value"
|
|
||||||
:placeholder="$t('repo.deploy_pipeline.variables.value')"
|
|
||||||
/>
|
|
||||||
<div class="w-10 flex-shrink-0">
|
|
||||||
<Button
|
|
||||||
v-if="i !== payload.variables.length - 1"
|
|
||||||
color="red"
|
|
||||||
class="ml-auto"
|
|
||||||
:title="$t('repo.deploy_pipeline.variables.delete')"
|
|
||||||
@click="deleteVar(i)"
|
|
||||||
>
|
|
||||||
<Icon name="remove" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InputField>
|
</InputField>
|
||||||
<Button type="submit" :text="$t('repo.deploy_pipeline.trigger')" />
|
<Button type="submit" :text="$t('repo.deploy_pipeline.trigger')" :disabled="!isFormValid" />
|
||||||
</form>
|
</form>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Popup>
|
</Popup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, toRef, watch } from 'vue';
|
import { computed, onMounted, ref, toRef } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import Button from '~/components/atomic/Button.vue';
|
import Button from '~/components/atomic/Button.vue';
|
||||||
import Icon from '~/components/atomic/Icon.vue';
|
|
||||||
import InputField from '~/components/form/InputField.vue';
|
import InputField from '~/components/form/InputField.vue';
|
||||||
|
import KeyValueEditor from '~/components/form/KeyValueEditor.vue';
|
||||||
import TextField from '~/components/form/TextField.vue';
|
import TextField from '~/components/form/TextField.vue';
|
||||||
import Panel from '~/components/layout/Panel.vue';
|
import Panel from '~/components/layout/Panel.vue';
|
||||||
import Popup from '~/components/layout/Popup.vue';
|
import Popup from '~/components/layout/Popup.vue';
|
||||||
|
@ -68,55 +51,37 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const apiClient = useApiClient();
|
const apiClient = useApiClient();
|
||||||
|
|
||||||
const repo = inject('repo');
|
const repo = inject('repo');
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const payload = ref<{ id: string; environment: string; task: string; variables: { name: string; value: string }[] }>({
|
const payload = ref<{
|
||||||
|
id: string;
|
||||||
|
environment: string;
|
||||||
|
task: string;
|
||||||
|
variables: Record<string, string>;
|
||||||
|
}>({
|
||||||
id: '',
|
id: '',
|
||||||
environment: '',
|
environment: '',
|
||||||
task: '',
|
task: '',
|
||||||
variables: [
|
variables: {},
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const pipelineOptions = computed(() => {
|
const isVariablesValid = ref(true);
|
||||||
const variables = Object.fromEntries(
|
|
||||||
payload.value.variables.filter((e) => e.name !== '').map((item) => [item.name, item.value]),
|
const isFormValid = computed(() => {
|
||||||
);
|
return payload.value.environment !== '' && isVariablesValid.value;
|
||||||
return {
|
|
||||||
...payload.value,
|
|
||||||
variables,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pipelineOptions = computed(() => ({
|
||||||
|
...payload.value,
|
||||||
|
variables: payload.value.variables,
|
||||||
|
}));
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
|
||||||
payload,
|
|
||||||
() => {
|
|
||||||
if (payload.value.variables[payload.value.variables.length - 1].name !== '') {
|
|
||||||
payload.value.variables.push({
|
|
||||||
name: '',
|
|
||||||
value: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
function deleteVar(index: number) {
|
|
||||||
payload.value.variables.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pipelineNumber = toRef(props, 'pipelineNumber');
|
const pipelineNumber = toRef(props, 'pipelineNumber');
|
||||||
async function triggerDeployPipeline() {
|
async function triggerDeployPipeline() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { useTabsClient } from '~/compositions/useTabs';
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
to: RouteLocationRaw;
|
to: RouteLocationRaw;
|
||||||
title: string;
|
title: string;
|
||||||
|
count?: number;
|
||||||
icon?: IconNames;
|
icon?: IconNames;
|
||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
matchChildren?: boolean;
|
matchChildren?: boolean;
|
||||||
|
@ -32,6 +33,7 @@ onMounted(() => {
|
||||||
tabs.value.push({
|
tabs.value.push({
|
||||||
to: props.to,
|
to: props.to,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
|
count: props.count,
|
||||||
icon: props.icon,
|
icon: props.icon,
|
||||||
iconClass: props.iconClass,
|
iconClass: props.iconClass,
|
||||||
matchChildren: props.matchChildren,
|
matchChildren: props.matchChildren,
|
||||||
|
|
|
@ -11,15 +11,17 @@
|
||||||
>
|
>
|
||||||
<Icon v-if="isExactActive || (isActive && tab.matchChildren)" name="chevron-right" class="md:hidden" />
|
<Icon v-if="isExactActive || (isActive && tab.matchChildren)" name="chevron-right" class="md:hidden" />
|
||||||
<Icon v-else name="blank" class="md:hidden" />
|
<Icon v-else name="blank" class="md:hidden" />
|
||||||
<span class="flex gap-2 items-center flex-row-reverse md:flex-row">
|
<span class="flex gap-2 items-center flex-row">
|
||||||
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" />
|
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" />
|
||||||
<span>{{ tab.title }}</span>
|
<span>{{ tab.title }}</span>
|
||||||
|
<CountBadge v-if="tab.count" :value="tab.count" />
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import CountBadge from '~/components/atomic/CountBadge.vue';
|
||||||
import Icon from '~/components/atomic/Icon.vue';
|
import Icon from '~/components/atomic/Icon.vue';
|
||||||
import { useTabsClient } from '~/compositions/useTabs';
|
import { useTabsClient } from '~/compositions/useTabs';
|
||||||
|
|
||||||
|
|
50
web/src/components/repo/RepoItem.vue
Normal file
50
web/src/components/repo/RepoItem.vue
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<router-link
|
||||||
|
v-if="repo"
|
||||||
|
:to="{ name: 'repo', params: { repoId: repo.id } }"
|
||||||
|
class="flex flex-col border rounded-md bg-wp-background-100 overflow-hidden p-4 border-wp-background-400 dark:bg-wp-background-200 cursor-pointer hover:shadow-md hover:bg-wp-background-300 dark:hover:bg-wp-background-300"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-[auto,1fr] gap-y-4 items-center">
|
||||||
|
<div class="text-wp-text-100 text-lg">{{ `${repo.owner} / ${repo.name}` }}</div>
|
||||||
|
<div class="ml-auto">
|
||||||
|
<Badge v-if="repo.visibility === RepoVisibility.Public" :label="$t('repo.visibility.public.public')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-2 text-wp-text-100 flex w-full gap-x-4">
|
||||||
|
<template v-if="lastPipeline">
|
||||||
|
<div class="flex flex-1 min-w-0 gap-x-1 items-center">
|
||||||
|
<PipelineStatusIcon v-if="lastPipeline" :status="lastPipeline.status" />
|
||||||
|
<span class="whitespace-nowrap overflow-hidden overflow-ellipsis">{{ shortMessage }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-shrink-0 gap-x-1 items-center ml-auto">
|
||||||
|
<Icon name="since" size="20" />
|
||||||
|
<span>{{ since }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-else class="flex gap-x-2">
|
||||||
|
<span>{{ $t('repo.pipeline.no_pipelines') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import Badge from '~/components/atomic/Badge.vue';
|
||||||
|
import Icon from '~/components/atomic/Icon.vue';
|
||||||
|
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
|
||||||
|
import usePipeline from '~/compositions/usePipeline';
|
||||||
|
import type { Repo } from '~/lib/api/types';
|
||||||
|
import { RepoVisibility } from '~/lib/api/types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
repo: Repo;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const lastPipeline = computed(() => props.repo.last_pipeline_item);
|
||||||
|
const { since, shortMessage } = usePipeline(lastPipeline);
|
||||||
|
</script>
|
|
@ -163,7 +163,7 @@ const hasLogs = computed(
|
||||||
// we do not have logs for skipped steps
|
// we do not have logs for skipped steps
|
||||||
repo?.value && pipeline.value && step.value && step.value.state !== 'skipped',
|
repo?.value && pipeline.value && step.value && step.value.state !== 'skipped',
|
||||||
);
|
);
|
||||||
const autoScroll = useStorage('log-auto-scroll', false);
|
const autoScroll = useStorage('woodpecker:log-auto-scroll', false);
|
||||||
const showActions = ref(false);
|
const showActions = ref(false);
|
||||||
const downloadInProgress = ref(false);
|
const downloadInProgress = ref(false);
|
||||||
const ansiUp = ref(new AnsiUp());
|
const ansiUp = ref(new AnsiUp());
|
||||||
|
|
55
web/src/compositions/useRepos.ts
Normal file
55
web/src/compositions/useRepos.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import type { Repo } from '~/lib/api/types';
|
||||||
|
import { usePipelineStore } from '~/store/pipelines';
|
||||||
|
|
||||||
|
export default function useRepos() {
|
||||||
|
const pipelineStore = usePipelineStore();
|
||||||
|
const lastAccess = useStorage('woodpecker:repo-last-access', new Map<number, number>());
|
||||||
|
|
||||||
|
function repoWithLastPipeline(repo: Repo): Repo {
|
||||||
|
if (repo.last_pipeline === undefined) {
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repo.last_pipeline_item?.number === repo.last_pipeline) {
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPipeline = pipelineStore.getPipeline(ref(repo.id), ref(repo.last_pipeline)).value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...repo,
|
||||||
|
last_pipeline_item: lastPipeline,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortReposByLastAccess(repos: Repo[]): Repo[] {
|
||||||
|
return repos.sort((a, b) => {
|
||||||
|
const aLastAccess = lastAccess.value.get(a.id) ?? 0;
|
||||||
|
const bLastAccess = lastAccess.value.get(b.id) ?? 0;
|
||||||
|
|
||||||
|
return bLastAccess - aLastAccess;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortReposByLastActivity(repos: Repo[]): Repo[] {
|
||||||
|
return repos.sort((a, b) => {
|
||||||
|
const aLastActivity = a.last_pipeline_item?.created ?? 0;
|
||||||
|
const bLastActivity = b.last_pipeline_item?.created ?? 0;
|
||||||
|
return bLastActivity - aLastActivity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLastAccess(repoId: number) {
|
||||||
|
lastAccess.value.set(repoId, Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortReposByLastAccess,
|
||||||
|
sortReposByLastActivity,
|
||||||
|
repoWithLastPipeline,
|
||||||
|
updateLastAccess,
|
||||||
|
};
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import { inject, provide } from './useInjectProvide';
|
||||||
export interface Tab {
|
export interface Tab {
|
||||||
to: RouteLocationRaw;
|
to: RouteLocationRaw;
|
||||||
title: string;
|
title: string;
|
||||||
|
count?: number;
|
||||||
icon?: IconNames;
|
icon?: IconNames;
|
||||||
iconClass?: string;
|
iconClass?: string;
|
||||||
matchChildren?: boolean;
|
matchChildren?: boolean;
|
||||||
|
|
|
@ -1,32 +1,19 @@
|
||||||
import { computed, ref } from 'vue';
|
import { useStorage } from '@vueuse/core';
|
||||||
|
import { computed } from 'vue';
|
||||||
const USER_CONFIG_KEY = 'woodpecker-user-config';
|
|
||||||
|
|
||||||
interface UserConfig {
|
interface UserConfig {
|
||||||
isPipelineFeedOpen: boolean;
|
isPipelineFeedOpen: boolean;
|
||||||
redirectUrl: string;
|
redirectUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultUserConfig: UserConfig = {
|
const config = useStorage<UserConfig>('woodpecker:user-config', {
|
||||||
isPipelineFeedOpen: false,
|
isPipelineFeedOpen: false,
|
||||||
redirectUrl: '',
|
redirectUrl: '',
|
||||||
};
|
});
|
||||||
|
|
||||||
function loadUserConfig(): UserConfig {
|
|
||||||
const lsData = localStorage.getItem(USER_CONFIG_KEY);
|
|
||||||
if (lsData === null) {
|
|
||||||
return defaultUserConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.parse(lsData) as UserConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = ref<UserConfig>(loadUserConfig());
|
|
||||||
|
|
||||||
export default () => ({
|
export default () => ({
|
||||||
setUserConfig<T extends keyof UserConfig>(key: T, value: UserConfig[T]): void {
|
setUserConfig<T extends keyof UserConfig>(key: T, value: UserConfig[T]): void {
|
||||||
config.value = { ...config.value, [key]: value };
|
config.value = { ...config.value, [key]: value };
|
||||||
localStorage.setItem(USER_CONFIG_KEY, JSON.stringify(config.value));
|
|
||||||
},
|
},
|
||||||
userConfig: computed(() => config.value),
|
userConfig: computed(() => config.value),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { Pipeline } from './pipeline';
|
||||||
|
|
||||||
// A version control repository.
|
// A version control repository.
|
||||||
export interface Repo {
|
export interface Repo {
|
||||||
// Is the repo currently active or not
|
// Is the repo currently active or not
|
||||||
|
@ -65,7 +67,9 @@ export interface Repo {
|
||||||
|
|
||||||
visibility: RepoVisibility;
|
visibility: RepoVisibility;
|
||||||
|
|
||||||
last_pipeline: number;
|
last_pipeline?: number;
|
||||||
|
|
||||||
|
last_pipeline_item?: Pipeline;
|
||||||
|
|
||||||
require_approval: RepoRequireApproval;
|
require_approval: RepoRequireApproval;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,14 @@ export const usePipelineStore = defineStore('pipelines', () => {
|
||||||
...(repoPipelines.get(pipeline.number) || {}),
|
...(repoPipelines.get(pipeline.number) || {}),
|
||||||
...pipeline,
|
...pipeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update last pipeline number for the repo
|
||||||
|
const repo = repoStore.repos.get(repoId);
|
||||||
|
if (repo?.last_pipeline !== undefined && repo.last_pipeline < pipeline.number) {
|
||||||
|
repo.last_pipeline = pipeline.number;
|
||||||
|
repoStore.setRepo(repo);
|
||||||
|
}
|
||||||
|
|
||||||
pipelines.set(repoId, repoPipelines);
|
pipelines.set(repoId, repoPipelines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,10 +33,14 @@ export const usePipelineStore = defineStore('pipelines', () => {
|
||||||
return computed(() => Array.from(pipelines.get(repoId.value)?.values() || []).sort(comparePipelines));
|
return computed(() => Array.from(pipelines.get(repoId.value)?.values() || []).sort(comparePipelines));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPipeline(repoId: Ref<number>, _pipelineNumber: Ref<string>) {
|
function getPipeline(repoId: Ref<number>, _pipelineNumber: Ref<string | number>) {
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
const pipelineNumber = Number.parseInt(_pipelineNumber.value, 10);
|
if (typeof _pipelineNumber.value === 'string') {
|
||||||
return pipelines.get(repoId.value)?.get(pipelineNumber);
|
const pipelineNumber = Number.parseInt(_pipelineNumber.value, 10);
|
||||||
|
return pipelines.get(repoId.value)?.get(pipelineNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipelines.get(repoId.value)?.get(_pipelineNumber.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,11 @@ import { computed, reactive, ref, type Ref } from 'vue';
|
||||||
import useApiClient from '~/compositions/useApiClient';
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
import type { Repo } from '~/lib/api/types';
|
import type { Repo } from '~/lib/api/types';
|
||||||
|
|
||||||
|
import { usePipelineStore } from './pipelines';
|
||||||
|
|
||||||
export const useRepoStore = defineStore('repos', () => {
|
export const useRepoStore = defineStore('repos', () => {
|
||||||
const apiClient = useApiClient();
|
const apiClient = useApiClient();
|
||||||
|
const pipelineStore = usePipelineStore();
|
||||||
|
|
||||||
const repos: Map<number, Repo> = reactive(new Map());
|
const repos: Map<number, Repo> = reactive(new Map());
|
||||||
const ownedRepoIds = ref<number[]>([]);
|
const ownedRepoIds = ref<number[]>([]);
|
||||||
|
@ -21,20 +24,30 @@ export const useRepoStore = defineStore('repos', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRepo(repo: Repo) {
|
function setRepo(repo: Repo) {
|
||||||
repos.set(repo.id, repo);
|
repos.set(repo.id, {
|
||||||
|
...repos.get(repo.id),
|
||||||
|
...repo,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRepo(repoId: number) {
|
async function loadRepo(repoId: number) {
|
||||||
const repo = await apiClient.getRepo(repoId);
|
const repo = await apiClient.getRepo(repoId);
|
||||||
repos.set(repo.id, repo);
|
setRepo(repo);
|
||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRepos() {
|
async function loadRepos() {
|
||||||
const _ownedRepos = await apiClient.getRepoList();
|
const _ownedRepos = await apiClient.getRepoList();
|
||||||
_ownedRepos.forEach((repo) => {
|
await Promise.all(
|
||||||
repos.set(repo.id, repo);
|
_ownedRepos.map(async (repo) => {
|
||||||
});
|
const lastPipeline = await apiClient.getPipelineList(repo.id, { page: 1, perPage: 1 });
|
||||||
|
if (lastPipeline.length === 1) {
|
||||||
|
pipelineStore.setPipeline(repo.id, lastPipeline[0]);
|
||||||
|
repo.last_pipeline = lastPipeline[0].number;
|
||||||
|
}
|
||||||
|
setRepo(repo);
|
||||||
|
}),
|
||||||
|
);
|
||||||
ownedRepoIds.value = _ownedRepos.map((repo) => repo.id);
|
ownedRepoIds.value = _ownedRepos.map((repo) => repo.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
|
|
||||||
export function getUserLanguage(): string {
|
export function getUserLanguage(): string {
|
||||||
const browserLocale = navigator.language.split('-')[0];
|
const browserLocale = navigator.language.split('-')[0];
|
||||||
const selectedLocale = useLocalStorage('woodpecker:locale', browserLocale).value;
|
const selectedLocale = useStorage('woodpecker:locale', browserLocale).value;
|
||||||
|
|
||||||
return selectedLocale;
|
return selectedLocale;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,26 @@
|
||||||
<Button :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" />
|
<Button :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<Transition name="fade" mode="out-in">
|
||||||
<ListItem v-for="repo in searchedRepos" :key="repo.id" :to="{ name: 'repo', params: { repoId: repo.id } }">
|
<div v-if="search === '' && repos.length > 0" class="gap-8 grid">
|
||||||
<span class="text-wp-text-100">{{ `${repo.owner} / ${repo.name}` }}</span>
|
<div v-if="reposLastAccess.length > 0" class="gap-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2">
|
||||||
</ListItem>
|
<RepoItem v-for="repo in reposLastAccess" :key="repo.id" :repo="repo" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<h2 class="text-wp-text-100 text-lg">{{ $t('all_repositories') }}</h2>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<RepoItem v-for="repo in reposLastActivity" :key="repo.id" :repo="repo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col">
|
||||||
|
<div v-if="reposLastActivity.length > 0" class="flex flex-col gap-4">
|
||||||
|
<RepoItem v-for="repo in reposLastActivity" :key="repo.id" :repo="repo" />
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-wp-text-100 text-lg text-center">{{ $t('no_search_results') }}</span>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</Scaffold>
|
</Scaffold>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -20,18 +35,36 @@
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import Button from '~/components/atomic/Button.vue';
|
import Button from '~/components/atomic/Button.vue';
|
||||||
import ListItem from '~/components/atomic/ListItem.vue';
|
|
||||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||||
|
import RepoItem from '~/components/repo/RepoItem.vue';
|
||||||
|
import useRepos from '~/compositions/useRepos';
|
||||||
import { useRepoSearch } from '~/compositions/useRepoSearch';
|
import { useRepoSearch } from '~/compositions/useRepoSearch';
|
||||||
import { useRepoStore } from '~/store/repos';
|
import { useRepoStore } from '~/store/repos';
|
||||||
|
|
||||||
const repoStore = useRepoStore();
|
const repoStore = useRepoStore();
|
||||||
const repos = computed(() => Object.values(repoStore.ownedRepos));
|
|
||||||
const search = ref('');
|
|
||||||
|
|
||||||
|
const { sortReposByLastAccess, sortReposByLastActivity, repoWithLastPipeline } = useRepos();
|
||||||
|
const repos = computed(() => Object.values(repoStore.ownedRepos).map((r) => repoWithLastPipeline(r)));
|
||||||
|
|
||||||
|
const reposLastAccess = computed(() => sortReposByLastAccess(repos.value || []).slice(0, 4));
|
||||||
|
|
||||||
|
const search = ref('');
|
||||||
const { searchedRepos } = useRepoSearch(repos, search);
|
const { searchedRepos } = useRepoSearch(repos, search);
|
||||||
|
const reposLastActivity = computed(() => sortReposByLastActivity(searchedRepos.value || []));
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await repoStore.loadRepos();
|
await repoStore.loadRepos();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -13,10 +13,8 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="flex flex-col gap-4">
|
||||||
<ListItem v-for="repo in searchedRepos" :key="repo.id" :to="{ name: 'repo', params: { repoId: repo.id } }">
|
<RepoItem v-for="repo in searchedRepos" :key="repo.id" :repo="repo" />
|
||||||
<span class="text-wp-text-100">{{ `${repo.owner} / ${repo.name}` }}</span>
|
|
||||||
</ListItem>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="(searchedRepos || []).length <= 0" class="text-center">
|
<div v-if="(searchedRepos || []).length <= 0" class="text-center">
|
||||||
<span class="text-wp-text-100 m-auto">{{ $t('repo.user_none') }}</span>
|
<span class="text-wp-text-100 m-auto">{{ $t('repo.user_none') }}</span>
|
||||||
|
@ -28,19 +26,25 @@
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import IconButton from '~/components/atomic/IconButton.vue';
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
import ListItem from '~/components/atomic/ListItem.vue';
|
|
||||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||||
|
import RepoItem from '~/components/repo/RepoItem.vue';
|
||||||
import { inject } from '~/compositions/useInjectProvide';
|
import { inject } from '~/compositions/useInjectProvide';
|
||||||
|
import useRepos from '~/compositions/useRepos';
|
||||||
import { useRepoSearch } from '~/compositions/useRepoSearch';
|
import { useRepoSearch } from '~/compositions/useRepoSearch';
|
||||||
import { useRepoStore } from '~/store/repos';
|
import { useRepoStore } from '~/store/repos';
|
||||||
|
|
||||||
const repoStore = useRepoStore();
|
const repoStore = useRepoStore();
|
||||||
|
const { repoWithLastPipeline } = useRepos();
|
||||||
|
|
||||||
const org = inject('org');
|
const org = inject('org');
|
||||||
const orgPermissions = inject('org-permissions');
|
const orgPermissions = inject('org-permissions');
|
||||||
|
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
const repos = computed(() => Array.from(repoStore.repos.values()).filter((repo) => repo.org_id === org.value?.id));
|
const repos = computed(() =>
|
||||||
|
Array.from(repoStore.repos.values())
|
||||||
|
.filter((repo) => repo.org_id === org.value?.id)
|
||||||
|
.map(repoWithLastPipeline),
|
||||||
|
);
|
||||||
const { searchedRepos } = useRepoSearch(repos, search);
|
const { searchedRepos } = useRepoSearch(repos, search);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
|
@ -67,6 +67,7 @@ import useAuthentication from '~/compositions/useAuthentication';
|
||||||
import useConfig from '~/compositions/useConfig';
|
import useConfig from '~/compositions/useConfig';
|
||||||
import { useForgeStore } from '~/compositions/useForgeStore';
|
import { useForgeStore } from '~/compositions/useForgeStore';
|
||||||
import useNotifications from '~/compositions/useNotifications';
|
import useNotifications from '~/compositions/useNotifications';
|
||||||
|
import useRepos from '~/compositions/useRepos';
|
||||||
import type { Forge, RepoPermissions } from '~/lib/api/types';
|
import type { Forge, RepoPermissions } from '~/lib/api/types';
|
||||||
import { usePipelineStore } from '~/store/pipelines';
|
import { usePipelineStore } from '~/store/pipelines';
|
||||||
import { useRepoStore } from '~/store/repos';
|
import { useRepoStore } from '~/store/repos';
|
||||||
|
@ -87,6 +88,7 @@ const router = useRouter();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const forgeStore = useForgeStore();
|
const forgeStore = useForgeStore();
|
||||||
|
const { updateLastAccess } = useRepos();
|
||||||
|
|
||||||
const repo = repoStore.getRepo(repositoryId);
|
const repo = repoStore.getRepo(repositoryId);
|
||||||
const repoPermissions = ref<RepoPermissions>();
|
const repoPermissions = ref<RepoPermissions>();
|
||||||
|
@ -121,6 +123,7 @@ async function loadRepo() {
|
||||||
if (repo.value) {
|
if (repo.value) {
|
||||||
forge.value = (await forgeStore.getForge(repo.value?.forge_id)).value;
|
forge.value = (await forgeStore.getForge(repo.value?.forge_id)).value;
|
||||||
}
|
}
|
||||||
|
updateLastAccess(repositoryId.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
|
@ -78,18 +78,16 @@
|
||||||
v-if="pipeline.errors && pipeline.errors.length > 0"
|
v-if="pipeline.errors && pipeline.errors.length > 0"
|
||||||
:to="{ name: 'repo-pipeline-errors' }"
|
:to="{ name: 'repo-pipeline-errors' }"
|
||||||
icon="attention"
|
icon="attention"
|
||||||
:title="
|
:title="pipeline.errors.some((e) => !e.is_warning) ? $t('repo.pipeline.errors') : $t('repo.pipeline.warnings')"
|
||||||
pipeline.errors.some((e) => !e.is_warning)
|
:count="pipeline.errors?.length"
|
||||||
? $t('repo.pipeline.errors', { count: pipeline.errors?.length })
|
|
||||||
: $t('repo.pipeline.warnings', { count: pipeline.errors?.length })
|
|
||||||
"
|
|
||||||
:icon-class="pipeline.errors.some((e) => !e.is_warning) ? 'text-wp-state-error-100' : 'text-wp-state-warn-100'"
|
:icon-class="pipeline.errors.some((e) => !e.is_warning) ? 'text-wp-state-error-100' : 'text-wp-state-warn-100'"
|
||||||
/>
|
/>
|
||||||
<Tab :to="{ name: 'repo-pipeline-config' }" :title="$t('repo.pipeline.config')" />
|
<Tab :to="{ name: 'repo-pipeline-config' }" :title="$t('repo.pipeline.config')" />
|
||||||
<Tab
|
<Tab
|
||||||
v-if="pipeline.changed_files && pipeline.changed_files.length > 0"
|
v-if="pipeline.changed_files && pipeline.changed_files.length > 0"
|
||||||
:to="{ name: 'repo-pipeline-changed-files' }"
|
:to="{ name: 'repo-pipeline-changed-files' }"
|
||||||
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })"
|
:title="$t('repo.pipeline.files')"
|
||||||
|
:count="pipeline.changed_files?.length"
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="repoPermissions && repoPermissions.push"
|
v-if="repoPermissions && repoPermissions.push"
|
||||||
|
|
|
@ -55,7 +55,7 @@ import type { Repo } from '~/lib/api/types';
|
||||||
const apiClient = useApiClient();
|
const apiClient = useApiClient();
|
||||||
const repo = inject<Ref<Repo>>('repo');
|
const repo = inject<Ref<Repo>>('repo');
|
||||||
|
|
||||||
const badgeType = useStorage('last-badge-type', 'markdown');
|
const badgeType = useStorage('woodpecker:last-badge-type', 'markdown');
|
||||||
|
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new Error('Unexpected: "repo" should be provided at this place');
|
throw new Error('Unexpected: "repo" should be provided at this place');
|
||||||
|
|
|
@ -74,10 +74,7 @@
|
||||||
</template>
|
</template>
|
||||||
</InputField>
|
</InputField>
|
||||||
|
|
||||||
<InputField
|
<InputField docs-url="docs/usage/project-settings#project-visibility" :label="$t('repo.visibility.visibility')">
|
||||||
docs-url="docs/usage/project-settings#project-visibility"
|
|
||||||
:label="$t('repo.settings.general.visibility.visibility')"
|
|
||||||
>
|
|
||||||
<RadioField v-model="repoSettings.visibility" :options="projectVisibilityOptions" />
|
<RadioField v-model="repoSettings.visibility" :options="projectVisibilityOptions" />
|
||||||
</InputField>
|
</InputField>
|
||||||
|
|
||||||
|
@ -215,18 +212,18 @@ onMounted(() => {
|
||||||
const projectVisibilityOptions: RadioOption[] = [
|
const projectVisibilityOptions: RadioOption[] = [
|
||||||
{
|
{
|
||||||
value: RepoVisibility.Public,
|
value: RepoVisibility.Public,
|
||||||
text: i18n.t('repo.settings.general.visibility.public.public'),
|
text: i18n.t('repo.visibility.public.public'),
|
||||||
description: i18n.t('repo.settings.general.visibility.public.desc'),
|
description: i18n.t('repo.visibility.public.desc'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: RepoVisibility.Internal,
|
value: RepoVisibility.Internal,
|
||||||
text: i18n.t('repo.settings.general.visibility.internal.internal'),
|
text: i18n.t('repo.visibility.internal.internal'),
|
||||||
description: i18n.t('repo.settings.general.visibility.internal.desc'),
|
description: i18n.t('repo.visibility.internal.desc'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: RepoVisibility.Private,
|
value: RepoVisibility.Private,
|
||||||
text: i18n.t('repo.settings.general.visibility.private.private'),
|
text: i18n.t('repo.visibility.private.private'),
|
||||||
description: i18n.t('repo.settings.general.visibility.private.desc'),
|
description: i18n.t('repo.visibility.private.desc'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { SUPPORTED_LOCALES } from 'virtual:vue-i18n-supported-locales';
|
import { SUPPORTED_LOCALES } from 'virtual:vue-i18n-supported-locales';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
@ -39,7 +39,7 @@ const localeOptions = computed(() =>
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const storedLocale = useLocalStorage('woodpecker:locale', locale.value);
|
const storedLocale = useStorage('woodpecker:locale', locale.value);
|
||||||
const selectedLocale = computed<string>({
|
const selectedLocale = computed<string>({
|
||||||
async set(_selectedLocale) {
|
async set(_selectedLocale) {
|
||||||
await setI18nLanguage(_selectedLocale);
|
await setI18nLanguage(_selectedLocale);
|
||||||
|
|
Loading…
Reference in a new issue