Skip to content

Commit 63d6eca

Browse files
committed
feat: better github integration release management
now using a table that visually pairs releases on either side * also added support for going straight to the github integration setup from codebase creation form, for people who would like to import their work from there
1 parent 292b766 commit 63d6eca

File tree

10 files changed

+567
-377
lines changed

10 files changed

+567
-377
lines changed

django/library/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,14 +1402,20 @@ def import_media(self, fileobj, user=None, title=None, images_only=True):
14021402
return image_metadata
14031403

14041404
@transaction.atomic
1405-
def get_or_create_draft(self):
1405+
def get_or_create_draft(self, initial_version: str | None = None):
14061406
existing_draft = self.releases.filter(
14071407
status=CodebaseRelease.Status.DRAFT
14081408
).first()
14091409
if existing_draft:
14101410
return existing_draft
14111411

1412-
draft_release = self.create_release()
1412+
# allow for overriding the version number of the initial draft
1413+
# this is currently used to create a less conflict-prone 0.0.1 draft when
1414+
# someone wants to import all their work with the github integration
1415+
if initial_version:
1416+
draft_release = self.create_release(version_number=initial_version)
1417+
else:
1418+
draft_release = self.create_release()
14131419
# reset fields that should not be copied over to a new draft
14141420
draft_release.doi = None
14151421
draft_release.release_notes = ""

django/library/views.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,8 @@ def get_queryset(self):
546546
def perform_create(self, serializer):
547547
super().perform_create(serializer)
548548
codebase = serializer.instance
549-
return codebase.get_or_create_draft()
549+
initial_version = self.request.query_params.get("initial_version")
550+
return codebase.get_or_create_draft(initial_version=initial_version)
550551

551552
def retrieve(self, request, *args, **kwargs):
552553
instance = self.get_object()
@@ -788,16 +789,16 @@ def submitter_installation_status(self, request, *args, **kwargs):
788789
@action(detail=False, methods=["post"])
789790
def setup_user_github_remote(self, request, *args, **kwargs):
790791
codebase = self.get_codebase()
791-
if not codebase.live:
792-
raise ValidationError("This model does not have any published releases")
793-
794792
installation = codebase.submitter.github_integration_app_installation
795793
if not installation:
796794
raise ValidationError(
797795
"Installation of the Github integration app is required"
798796
)
799797

800798
is_preexisting = bool(request.data.get("is_preexisting", False))
799+
if not codebase.live and not is_preexisting:
800+
raise ValidationError("This model does not have any published releases")
801+
801802
repo_name = request.data.get("repo_name")
802803
if not repo_name:
803804
raise ValidationError("Repository name is required")

frontend/src/components/CodebaseEditForm.vue

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,32 +51,39 @@
5151
class="mb-3"
5252
name="repositoryUrl"
5353
label="Version Control Repository URL (reference only)"
54-
help="Link to your model's version control repository (GitHub, GitLab, BitBucket, etc.) for reference only that will not be used for synchronization."
54+
help="Link to your model's version control repository (GitHub, GitLab, BitBucket, etc.). For reference only, you can connect a GitHub repository with the button below, or later on."
5555
/>
5656
<FormAlert :validation-errors="Object.values(errors)" :server-errors="serverErrors" />
57-
<button
58-
v-if="!asModal"
59-
type="submit"
60-
class="btn btn-primary"
61-
:disabled="isLoading"
62-
data-cy="next"
63-
>
64-
{{ props.identifier ? "Update" : "Next" }}
65-
</button>
57+
<div v-if="!asModal" class="d-flex gap-2">
58+
<button type="submit" class="btn btn-primary" :disabled="isLoading" data-cy="next">
59+
{{ props.identifier ? "Update" : "Continue to upload model" }}
60+
</button>
61+
<button
62+
v-if="!props.identifier"
63+
type="submit"
64+
class="btn btn-outline-gray"
65+
:disabled="isLoading"
66+
data-cy="go-github-config"
67+
@click="goToGithubConfig = true"
68+
>
69+
Import model from GitHub
70+
</button>
71+
</div>
6672
</form>
6773
</template>
6874

6975
<script setup lang="ts">
7076
import * as yup from "yup";
71-
import { onBeforeUnmount, onMounted } from "vue";
77+
import { onBeforeUnmount, onMounted, ref } from "vue";
7278
import TextField from "@/components/form/TextField.vue";
7379
import TextareaField from "@/components/form/TextareaField.vue";
7480
import MarkdownField from "@/components/form/MarkdownField.vue";
7581
import TaggerField from "@/components/form/TaggerField.vue";
7682
import HoneypotField from "@/components/form/HoneypotField.vue";
7783
import FormAlert from "@/components/form/FormAlert.vue";
7884
import { useForm } from "@/composables/form";
79-
import { useCodebaseAPI, useReleaseEditorAPI } from "@/composables/api";
85+
import { type RequestOptions, useCodebaseAPI, useReleaseEditorAPI } from "@/composables/api";
86+
import { useGitRemotesAPI } from "@/composables/api/git";
8087
8188
const props = withDefaults(
8289
defineProps<{
@@ -106,6 +113,7 @@ type CodebaseEditFields = yup.InferType<typeof schema>;
106113
107114
const { data, serverErrors, create, retrieve, update, isLoading, detailUrl } = useCodebaseAPI();
108115
const { editUrl } = useReleaseEditorAPI();
116+
const goToGithubConfig = ref(false);
109117
110118
const {
111119
errors,
@@ -137,27 +145,40 @@ onBeforeUnmount(() => {
137145
removeUnsavedAlertListener();
138146
});
139147
140-
function nextUrl(identifier: string) {
141-
if (props.identifier) {
142-
return detailUrl(props.identifier);
143-
} else {
144-
const versionNumber = values.latestVersionNumber || "1.0.0";
145-
return editUrl(identifier, versionNumber);
146-
}
148+
function githubConfigUrl(identifier: string) {
149+
const { detailUrl: gitDetailUrl } = useGitRemotesAPI(identifier);
150+
return gitDetailUrl("");
151+
}
152+
153+
function redirectUrl(identifier: string, useGithubConfig: boolean) {
154+
if (useGithubConfig) return githubConfigUrl(identifier);
155+
if (props.identifier) return detailUrl(props.identifier);
156+
const versionNumber = values.latestVersionNumber || "1.0.0";
157+
return editUrl(identifier, versionNumber);
147158
}
148159
149160
async function createOrUpdate() {
161+
const useGithubConfig = goToGithubConfig.value && !props.identifier;
150162
const onSuccess = (response: any) => {
163+
const destination = useGithubConfig;
151164
if (props.asModal) {
152165
emit("success");
153166
} else {
154-
window.location.href = nextUrl(response.data.identifier);
167+
window.location.href = redirectUrl(response.data.identifier, destination);
155168
}
156169
};
157-
if (props.identifier) {
158-
await update(props.identifier, values, { onSuccess });
159-
} else {
160-
await create(values, { onSuccess });
170+
const requestOptions: RequestOptions = { onSuccess };
171+
if (useGithubConfig) {
172+
requestOptions.config = { params: { initial_version: "0.0.1" } };
173+
}
174+
try {
175+
if (props.identifier) {
176+
await update(props.identifier, values, requestOptions);
177+
} else {
178+
await create(values, requestOptions);
179+
}
180+
} finally {
181+
goToGithubConfig.value = false;
161182
}
162183
}
163184
</script>

frontend/src/components/GitHubIntegrationConfiguration.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
</div>
3333
<FormAlert :validation-errors="[]" :server-errors="serverErrors" />
3434
</div>
35-
<div v-if="showReleaseManagement">
35+
<div v-if="showReleaseManagement" class="border rounded p-3">
3636
<ReleaseManagementSection
3737
:codebase-identifier="codebaseIdentifier"
3838
:active-remote="activeRemote"
Lines changed: 11 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,26 @@
11
<template>
2-
<li
3-
class="list-group-item"
4-
:class="{
5-
'opacity-50': release.createdByIntegration,
6-
}"
7-
>
8-
<div class="d-flex align-items-center justify-content-between mb-2">
2+
<li class="list-group-item border-0 p-2 rounded" :class="itemClass">
3+
<div class="d-flex flex-column gap-1">
94
<div class="d-flex align-items-center gap-2">
105
<a :href="release.htmlUrl" target="_blank">
116
<i class="fab fa-github"></i>
127
</a>
138
<h6 class="mb-0 fw-bold">{{ release.name || release.tagName }}</h6>
14-
<span v-if="release.createdByIntegration" class="badge bg-secondary"
15-
><i class="fas fa-download"></i> Created by integration</span
16-
>
9+
<span class="badge bg-gray">
10+
<i class="fas fa-tag"></i>
11+
{{ release.tagName }}
12+
</span>
1713
</div>
18-
<div class="d-flex align-items-center gap-2">
19-
<span v-if="release.importedSyncState?.status" class="badge" :class="statusBadgeClass">{{
20-
statusLabel
21-
}}</span>
22-
<BootstrapTooltip
23-
v-if="release.importedSyncState?.status === 'ERROR' && errorMessage"
24-
:title="errorMessage"
25-
icon-class="fas fa-exclamation-triangle text-danger"
26-
placement="bottom"
27-
/>
28-
<button
29-
v-if="release.importedSyncState?.canReimport && release.importedSyncState?.status"
30-
class="btn btn-link btn-sm"
31-
:disabled="isImporting"
32-
title="Retry import"
33-
@click="handleImport"
34-
>
35-
<i v-if="!isImporting" class="fas fa-redo"></i>
36-
<span
37-
v-else
38-
class="spinner-border spinner-border-sm"
39-
role="status"
40-
aria-hidden="true"
41-
></span>
42-
</button>
43-
<button
44-
v-else-if="!release.createdByIntegration"
45-
class="btn btn-primary btn-sm"
46-
:disabled="isImporting"
47-
@click="handleImport"
48-
>
49-
<span
50-
v-if="isImporting"
51-
class="spinner-border spinner-border-sm me-2"
52-
role="status"
53-
aria-hidden="true"
54-
></span>
55-
<i v-else class="fas fa-download me-1"></i>
56-
Import
57-
</button>
14+
<div class="text-muted small">
15+
Released {{ (release.publishedAt || release.createdAt)?.split("T")[0] }}
5816
</div>
5917
</div>
60-
61-
<div class="d-flex align-items-center gap-2 text-muted small">
62-
<span>Released {{ (release.publishedAt || release.createdAt)?.split("T")[0] }}</span>
63-
</div>
64-
65-
<BootstrapModal id="versionModal" title="Specify Version" ref="versionModal" centered>
66-
<template #body>
67-
<p class="text-muted mb-3">
68-
This release does not conform to semantic versioning. Please specify a version number.
69-
</p>
70-
<div class="mb-3">
71-
<label for="custom-version" class="form-label">Version Number</label>
72-
<input
73-
id="custom-version"
74-
type="text"
75-
class="form-control"
76-
placeholder="e.g., 1.0.0"
77-
v-model="customVersion"
78-
/>
79-
</div>
80-
</template>
81-
<template #footer>
82-
<button type="button" class="btn btn-outline-gray" data-bs-dismiss="modal">Cancel</button>
83-
<button
84-
type="button"
85-
class="btn btn-primary"
86-
:disabled="!customVersion.trim()"
87-
@click="handleVersionSubmit"
88-
>
89-
Pull Release
90-
</button>
91-
</template>
92-
</BootstrapModal>
9318
</li>
9419
</template>
9520

9621
<script setup lang="ts">
97-
import { ref, computed } from "vue";
98-
import { useGitRemotesAPI } from "@/composables/api/git";
22+
import { computed } from "vue";
9923
import type { GitHubRelease } from "@/types";
100-
import BootstrapModal from "@/components/BootstrapModal.vue";
101-
import BootstrapTooltip from "@/components/BootstrapTooltip.vue";
10224
10325
export interface GitHubReleaseItemProps {
10426
release: GitHubRelease;
@@ -107,71 +29,6 @@ export interface GitHubReleaseItemProps {
10729
10830
const props = defineProps<GitHubReleaseItemProps>();
10931
110-
const emit = defineEmits<{
111-
"import-started": [release: GitHubReleaseItemProps["release"]];
112-
}>();
113-
114-
const isImporting = ref(false);
115-
const versionModal = ref();
116-
const customVersion = ref("");
117-
118-
const { importGitHubRelease } = useGitRemotesAPI(props.codebaseIdentifier);
119-
120-
const statusLabel = computed(() => {
121-
const state = props.release.importedSyncState;
122-
if (!state) return "";
123-
switch (state.status) {
124-
case "SUCCESS":
125-
return "Success";
126-
case "RUNNING":
127-
return "In Progress";
128-
case "ERROR":
129-
return "Failed";
130-
default:
131-
return null;
132-
}
133-
});
134-
135-
const statusBadgeClass = computed(() => {
136-
const state = props.release.importedSyncState;
137-
if (!state) return null;
138-
switch (state.status) {
139-
case "SUCCESS":
140-
return "bg-success";
141-
case "RUNNING":
142-
return "bg-warning";
143-
case "ERROR":
144-
return "bg-danger";
145-
default:
146-
return null;
147-
}
148-
});
149-
150-
const errorMessage = computed(() => props.release.importedSyncState?.errorMessage || "");
151-
152-
const handleImport = async () => {
153-
if (!props.release.hasSemanticVersioning) {
154-
customVersion.value = "";
155-
versionModal.value?.show();
156-
return;
157-
}
158-
await importRelease();
159-
};
160-
161-
async function importRelease(version?: string) {
162-
isImporting.value = true;
163-
try {
164-
await importGitHubRelease(props.release.id, version);
165-
emit("import-started", props.release);
166-
} finally {
167-
isImporting.value = false;
168-
}
169-
}
170-
171-
async function handleVersionSubmit() {
172-
if (!customVersion.value.trim()) return;
173-
await importRelease(customVersion.value);
174-
versionModal.value?.hide();
175-
customVersion.value = "";
176-
}
32+
const isOriginal = computed(() => !props.release.createdByIntegration);
33+
const itemClass = computed(() => (isOriginal.value ? "bg-blue-gray" : "border"));
17734
</script>

0 commit comments

Comments
 (0)