|
|
|
|
@@ -1,26 +1,138 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
import { computed, onMounted, ref } from 'vue';
|
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
import {
|
|
|
|
|
RELEASE_COMMIT_STATUSES,
|
|
|
|
|
RELEASE_UPDATE_AUDIENCES,
|
|
|
|
|
RELEASE_UPDATE_CATEGORIES,
|
|
|
|
|
RELEASE_UPDATE_IMPORTANCE,
|
|
|
|
|
useReleaseCommunicationsStore,
|
|
|
|
|
} from '@/features/release-communications/stores/releaseCommunicationsStore.js';
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const store = useReleaseCommunicationsStore();
|
|
|
|
|
const importJson = ref('[]');
|
|
|
|
|
const selectedCommitShas = ref([]);
|
|
|
|
|
const importResult = ref(null);
|
|
|
|
|
const copyResult = ref('');
|
|
|
|
|
const repositoryImportForm = reactive({
|
|
|
|
|
sourceBranch: 'main',
|
|
|
|
|
deploymentLabel: '',
|
|
|
|
|
limit: 50,
|
|
|
|
|
since: '',
|
|
|
|
|
until: '',
|
|
|
|
|
});
|
|
|
|
|
const updateForm = reactive({
|
|
|
|
|
title: '',
|
|
|
|
|
summary: '',
|
|
|
|
|
body: '',
|
|
|
|
|
category: 'Improvement',
|
|
|
|
|
importance: 'Normal',
|
|
|
|
|
audience: 'Everyone',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const updateOptions = computed(() =>
|
|
|
|
|
store.developerUpdates.map(update => ({ title: update.title, value: update.id }))
|
|
|
|
|
);
|
|
|
|
|
const selectedCommits = computed(() => {
|
|
|
|
|
const selected = new Set(selectedCommitShas.value);
|
|
|
|
|
return store.filteredCommits.filter(commit => selected.has(commit.sha));
|
|
|
|
|
});
|
|
|
|
|
const selectedCommitText = computed(() =>
|
|
|
|
|
selectedCommits.value.map(commit => [
|
|
|
|
|
commit.sha,
|
|
|
|
|
commit.subject,
|
|
|
|
|
commit.authorName ? `Author: ${commit.authorName}` : '',
|
|
|
|
|
commit.committedAt ? `Committed: ${formatDate(commit.committedAt)}` : '',
|
|
|
|
|
].filter(Boolean).join('\n')).join('\n\n')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await Promise.all([store.loadDeveloperUpdates(), store.loadCommits()]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
watch(selectedCommits, commits => {
|
|
|
|
|
if (!commits.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!updateForm.title) {
|
|
|
|
|
updateForm.title = commits[0].subject;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!updateForm.summary) {
|
|
|
|
|
updateForm.summary = commits.slice(0, 3).map(commit => commit.subject).join('; ').slice(0, 512);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!updateForm.body) {
|
|
|
|
|
updateForm.body = selectedCommitText.value;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function importFromRepository() {
|
|
|
|
|
importResult.value = await store.importCommits({
|
|
|
|
|
sourceBranch: repositoryImportForm.sourceBranch || null,
|
|
|
|
|
deploymentLabel: repositoryImportForm.deploymentLabel || null,
|
|
|
|
|
limit: Number(repositoryImportForm.limit) || 50,
|
|
|
|
|
since: repositoryImportForm.since || null,
|
|
|
|
|
until: repositoryImportForm.until || null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function importPayload() {
|
|
|
|
|
const commits = JSON.parse(importJson.value);
|
|
|
|
|
await store.importCommits({ commits });
|
|
|
|
|
importResult.value = await store.importCommits({ commits });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSelected(sha) {
|
|
|
|
|
return selectedCommitShas.value.includes(sha);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleCommit(sha, selected) {
|
|
|
|
|
selectedCommitShas.value = selected
|
|
|
|
|
? [...new Set([...selectedCommitShas.value, sha])]
|
|
|
|
|
: selectedCommitShas.value.filter(selectedSha => selectedSha !== sha);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearSelection() {
|
|
|
|
|
selectedCommitShas.value = [];
|
|
|
|
|
copyResult.value = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copySelectedCommits() {
|
|
|
|
|
await navigator.clipboard.writeText(selectedCommitText.value);
|
|
|
|
|
copyResult.value = t('releaseCommunications.commits.copied');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyCommit(commit) {
|
|
|
|
|
await navigator.clipboard.writeText(`${commit.sha}\n${commit.subject}`);
|
|
|
|
|
copyResult.value = t('releaseCommunications.commits.copied');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createUpdateFromSelection() {
|
|
|
|
|
const commits = selectedCommits.value;
|
|
|
|
|
if (!commits.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const firstCommit = commits[0];
|
|
|
|
|
const lastCommit = commits[commits.length - 1];
|
|
|
|
|
const update = await store.saveDeveloperUpdate({
|
|
|
|
|
title: updateForm.title,
|
|
|
|
|
summary: updateForm.summary,
|
|
|
|
|
body: updateForm.body,
|
|
|
|
|
category: updateForm.category,
|
|
|
|
|
importance: updateForm.importance,
|
|
|
|
|
audience: updateForm.audience,
|
|
|
|
|
deploymentLabel: repositoryImportForm.deploymentLabel,
|
|
|
|
|
buildVersion: '',
|
|
|
|
|
commitRange: `${firstCommit.shortSha}..${lastCommit.shortSha}`,
|
|
|
|
|
});
|
|
|
|
|
await store.linkCommitsToUpdate(selectedCommitShas.value, update.id);
|
|
|
|
|
clearSelection();
|
|
|
|
|
await router.push({ name: 'developer-release-updates' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDate(value) {
|
|
|
|
|
@@ -40,13 +152,49 @@
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<section class="import-panel">
|
|
|
|
|
<div class="repo-import-grid">
|
|
|
|
|
<v-text-field v-model="repositoryImportForm.sourceBranch" :label="t('releaseCommunications.commits.branch')" density="compact" variant="outlined" hide-details />
|
|
|
|
|
<v-text-field v-model="repositoryImportForm.deploymentLabel" :label="t('releaseCommunications.deploymentLabel')" density="compact" variant="outlined" hide-details />
|
|
|
|
|
<v-text-field v-model="repositoryImportForm.limit" :label="t('releaseCommunications.commits.limit')" type="number" min="1" max="100" density="compact" variant="outlined" hide-details />
|
|
|
|
|
<v-text-field v-model="repositoryImportForm.since" :label="t('releaseCommunications.commits.since')" type="date" density="compact" variant="outlined" hide-details />
|
|
|
|
|
<v-text-field v-model="repositoryImportForm.until" :label="t('releaseCommunications.commits.until')" type="date" density="compact" variant="outlined" hide-details />
|
|
|
|
|
<v-btn :loading="store.isImporting" @click="importFromRepository">{{ t('releaseCommunications.commits.fetch') }}</v-btn>
|
|
|
|
|
</div>
|
|
|
|
|
<v-textarea
|
|
|
|
|
v-model="importJson"
|
|
|
|
|
:label="t('releaseCommunications.commits.importJson')"
|
|
|
|
|
rows="5"
|
|
|
|
|
rows="3"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
/>
|
|
|
|
|
<v-btn :loading="store.isImporting" @click="importPayload">{{ t('releaseCommunications.commits.import') }}</v-btn>
|
|
|
|
|
<small v-if="importResult">
|
|
|
|
|
{{ t('releaseCommunications.commits.importResult', { imported: importResult.importedCount, updated: importResult.updatedCount, skipped: importResult.skippedCount }) }}
|
|
|
|
|
</small>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section
|
|
|
|
|
v-if="selectedCommits.length"
|
|
|
|
|
class="selection-panel"
|
|
|
|
|
>
|
|
|
|
|
<div class="selection-header">
|
|
|
|
|
<strong>{{ t('releaseCommunications.commits.selected', { count: selectedCommits.length }) }}</strong>
|
|
|
|
|
<div class="commit-actions">
|
|
|
|
|
<v-btn variant="outlined" size="small" @click="copySelectedCommits">{{ t('releaseCommunications.commits.copySelected') }}</v-btn>
|
|
|
|
|
<v-btn variant="text" size="small" @click="clearSelection">{{ t('releaseCommunications.commits.clearSelection') }}</v-btn>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<small v-if="copyResult">{{ copyResult }}</small>
|
|
|
|
|
<form class="update-entry-form" @submit.prevent="createUpdateFromSelection">
|
|
|
|
|
<v-text-field v-model="updateForm.title" :label="t('title')" density="compact" variant="outlined" />
|
|
|
|
|
<v-textarea v-model="updateForm.summary" :label="t('releaseCommunications.summary')" rows="2" variant="outlined" />
|
|
|
|
|
<v-textarea v-model="updateForm.body" :label="t('releaseCommunications.body')" rows="5" variant="outlined" />
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
<v-select v-model="updateForm.category" :items="RELEASE_UPDATE_CATEGORIES" :label="t('releaseCommunications.category')" density="compact" variant="outlined" />
|
|
|
|
|
<v-select v-model="updateForm.importance" :items="RELEASE_UPDATE_IMPORTANCE" :label="t('releaseCommunications.importance')" density="compact" variant="outlined" />
|
|
|
|
|
<v-select v-model="updateForm.audience" :items="RELEASE_UPDATE_AUDIENCES" :label="t('releaseCommunications.audience')" density="compact" variant="outlined" />
|
|
|
|
|
</div>
|
|
|
|
|
<v-btn type="submit" :loading="store.isSaving">{{ t('releaseCommunications.commits.createUpdate') }}</v-btn>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="filter-panel">
|
|
|
|
|
@@ -63,9 +211,15 @@
|
|
|
|
|
:key="commit.sha"
|
|
|
|
|
class="commit-row"
|
|
|
|
|
>
|
|
|
|
|
<v-checkbox-btn
|
|
|
|
|
:model-value="isSelected(commit.sha)"
|
|
|
|
|
:aria-label="t('releaseCommunications.commits.selectCommit')"
|
|
|
|
|
@update:model-value="value => toggleCommit(commit.sha, value)"
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<code>{{ commit.shortSha }}</code>
|
|
|
|
|
<strong>{{ commit.subject }}</strong>
|
|
|
|
|
<small>{{ commit.sha }}</small>
|
|
|
|
|
<small>{{ commit.authorName }} / {{ formatDate(commit.committedAt) }}</small>
|
|
|
|
|
</div>
|
|
|
|
|
<span>{{ commit.communicationStatus }}</span>
|
|
|
|
|
@@ -80,6 +234,7 @@
|
|
|
|
|
@update:model-value="value => value ? store.linkCommit(commit.sha, value) : store.unlinkCommit(commit.sha)"
|
|
|
|
|
/>
|
|
|
|
|
<div class="commit-actions">
|
|
|
|
|
<v-btn size="small" variant="outlined" @click="copyCommit(commit)">{{ t('releaseCommunications.commits.copy') }}</v-btn>
|
|
|
|
|
<v-btn size="small" variant="outlined" @click="store.markCommitInternalOnly(commit.sha)">{{ t('releaseCommunications.commits.internalOnly') }}</v-btn>
|
|
|
|
|
<v-btn size="small" variant="outlined" @click="store.ignoreCommit(commit.sha)">{{ t('releaseCommunications.commits.ignore') }}</v-btn>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -98,7 +253,9 @@
|
|
|
|
|
.page-header,
|
|
|
|
|
.filter-panel,
|
|
|
|
|
.commit-row,
|
|
|
|
|
.commit-actions {
|
|
|
|
|
.commit-actions,
|
|
|
|
|
.form-row,
|
|
|
|
|
.selection-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
@@ -116,6 +273,7 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.import-panel,
|
|
|
|
|
.selection-panel,
|
|
|
|
|
.filter-panel,
|
|
|
|
|
.commit-row {
|
|
|
|
|
border: 1px solid #d8dee8;
|
|
|
|
|
@@ -128,6 +286,29 @@
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.import-panel,
|
|
|
|
|
.selection-panel,
|
|
|
|
|
.update-entry-form {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.repo-import-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(130px, 1fr) minmax(150px, 1fr) minmax(90px, 0.4fr) minmax(130px, 0.7fr) minmax(130px, 0.7fr) auto;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.selection-header {
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-row {
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.commit-table {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
@@ -136,7 +317,7 @@
|
|
|
|
|
.commit-row {
|
|
|
|
|
align-items: center;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) 120px minmax(220px, 320px) auto;
|
|
|
|
|
grid-template-columns: 44px minmax(0, 1fr) 120px minmax(220px, 320px) auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.commit-row > div:first-child {
|
|
|
|
|
@@ -146,7 +327,9 @@
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.filter-panel,
|
|
|
|
|
.commit-row {
|
|
|
|
|
.commit-row,
|
|
|
|
|
.repo-import-grid,
|
|
|
|
|
.form-row {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|