Skip to content

Commit 504b74f

Browse files
Move application wizard to full page workspace
1 parent e60df89 commit 504b74f

7 files changed

Lines changed: 259 additions & 67 deletions

File tree

‎web/src/App.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ onUnmounted(() => document.removeEventListener("keydown", keydown));
7979
></RouterLink
8080
>
8181
</aside>
82-
<main>
82+
<main :class="{ 'main-wizard': route.path.endsWith('/applications/new') }">
8383
<header>
8484
<button
8585
class="menu"
@@ -102,7 +102,7 @@ onUnmounted(() => document.removeEventListener("keydown", keydown));
102102
}}</span></RouterLink
103103
>
104104
</header>
105-
<section class="content"><RouterView /></section>
105+
<section class="content" :class="{ 'content-wizard': route.path.endsWith('/applications/new') }"><RouterView /></section>
106106
</main>
107107
<ToastContainer />
108108
</div>
Lines changed: 29 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
<script setup lang="ts">
2-
import BaseModal from "../ui/BaseModal.vue";
32
import WizardStepper from "./WizardStepper.vue";
43
54
defineProps<{
6-
open: boolean;
7-
editing?: boolean;
85
step: number;
96
busy?: boolean;
107
validating?: boolean;
@@ -17,41 +14,39 @@ const emit = defineEmits<{
1714
next: [];
1815
draft: [];
1916
create: [];
20-
save: [];
2117
}>();
2218
</script>
2319

2420
<template>
25-
<BaseModal
26-
:open="open"
27-
:title="editing ? 'Editar aplicação' : 'Nova aplicação'"
28-
description="Defina a origem e revise a configuração antes de criar."
29-
@close="emit('close')"
30-
>
31-
<div v-if="!editing">
21+
<main class="application-wizard-page">
22+
<header class="application-wizard-head">
23+
<div>
24+
<p class="kicker">NOVA APLICAÇÃO</p>
25+
<h1>Nova aplicação</h1>
26+
<p class="muted">Defina a origem e revise a configuração antes de criar.</p>
27+
</div>
28+
<button class="secondary wizard-back" type="button" @click="emit('close')">
29+
← Voltar ao projeto
30+
</button>
31+
</header>
32+
<div class="application-wizard-nav">
3233
<WizardStepper :step="step" />
33-
<slot />
3434
</div>
35-
<slot v-else name="edit" />
36-
<template #footer>
37-
<button class="secondary" type="button" :disabled="busy" @click="emit('close')">
38-
Cancelar
39-
</button>
40-
<button v-if="!editing && step > 1" class="secondary" type="button" :disabled="busy" @click="emit('back')">
41-
Voltar
42-
</button>
43-
<button v-if="!editing && step < 4" class="primary" type="button" :disabled="busy || validating || canContinue === false" @click="emit('next')">
44-
{{ validating ? "Validando…" : step === 3 && sourceType === "compose" ? "Validar e continuar" : "Continuar" }}
45-
</button>
46-
<button v-if="!editing && step === 4" class="secondary" type="button" :disabled="busy" @click="emit('draft')">
47-
Salvar como rascunho
48-
</button>
49-
<button v-if="!editing && step === 4" class="primary" form="new-app-form" type="submit" :disabled="busy">
50-
{{ busy ? "Criando…" : "Criar aplicação" }}
51-
</button>
52-
<button v-if="editing" class="primary" type="button" :disabled="busy" @click="emit('save')">
53-
{{ busy ? "Salvando…" : "Salvar alterações" }}
54-
</button>
55-
</template>
56-
</BaseModal>
35+
<section class="application-wizard-body">
36+
<slot />
37+
</section>
38+
<footer class="application-wizard-foot">
39+
<button v-if="step > 1" class="secondary" type="button" :disabled="busy" @click="emit('back')">Voltar</button>
40+
<span v-else />
41+
<div class="wizard-foot-actions">
42+
<button v-if="step < 4" class="primary" type="button" :disabled="busy || validating || canContinue === false" @click="emit('next')">
43+
{{ validating ? "Validando…" : step === 3 && sourceType === "compose" ? "Validar e continuar" : "Continuar" }}
44+
</button>
45+
<template v-else>
46+
<button class="secondary" type="button" :disabled="busy" @click="emit('draft')">Salvar como rascunho</button>
47+
<button class="primary" type="button" :disabled="busy" @click="emit('create')">{{ busy ? "Criando…" : "Criar aplicação" }}</button>
48+
</template>
49+
</div>
50+
</footer>
51+
</main>
5752
</template>

‎web/src/main.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ const router = createRouter({
3333
component: () => import("./views/ProjectDetail.vue"),
3434
meta: { auth: true, title: "Projeto" },
3535
},
36+
{
37+
path: "/projects/:projectId/applications/new",
38+
component: () => import("./views/ApplicationNew.vue"),
39+
meta: { auth: true, title: "Nova aplicação" },
40+
},
3641
{
3742
path: "/projects/:projectId/applications/:applicationId",
3843
component: () => import("./views/ApplicationDetail.vue"),

‎web/src/style.css‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ main {
165165
min-width: 0;
166166
flex: 1;
167167
}
168+
.main-wizard { display: flex; height: 100vh; min-height: 0; flex-direction: column; overflow: hidden; }
168169
header {
169170
display: flex;
170171
height: 72px;
@@ -211,6 +212,7 @@ header {
211212
margin: auto;
212213
padding: 56px 42px 80px;
213214
}
215+
.content-wizard { flex: 1; min-height: 0; width: 100%; max-width: 1160px; margin: 0 auto; padding: 0 42px; overflow: hidden; }
214216
.hero {
215217
display: flex;
216218
align-items: end;
@@ -442,6 +444,61 @@ h2 {
442444
.identity-preview { margin: 16px 0; max-width: 460px; }
443445
.catalog-toolbar { display: flex; gap: 10px; }
444446
.catalog-toolbar input, .catalog-toolbar select { min-width: 0; }
447+
448+
.application-wizard-page {
449+
height: 100%;
450+
min-height: 0;
451+
display: flex;
452+
flex-direction: column;
453+
margin: 0;
454+
padding-top: 8px;
455+
background: #101216;
456+
}
457+
.application-wizard-head,
458+
.application-wizard-nav,
459+
.application-wizard-foot {
460+
flex: 0 0 auto;
461+
padding: 20px 32px;
462+
border-bottom: 1px solid #292d35;
463+
background: #14161a;
464+
}
465+
.application-wizard-head { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
466+
.application-wizard-head { min-height: 108px; padding-top: 24px; padding-bottom: 18px; }
467+
.application-wizard-head h1 { margin: 2px 0 4px; }
468+
.application-wizard-head .muted { margin: 0; }
469+
.application-wizard-nav { min-height: 68px; padding-top: 14px; padding-bottom: 12px; }
470+
.application-wizard-nav .wizard-stepper { margin: 0; }
471+
.application-wizard-body { flex: 1; min-height: 0; overflow: hidden; padding: 24px 32px; }
472+
.wizard-step-content { width: 100%; height: 100%; min-height: 0; overflow: auto; max-width: 1280px; margin: 0 auto; }
473+
.application-wizard-foot { display: flex; align-items: center; justify-content: space-between; border-top: 1px solid #292d35; border-bottom: 0; }
474+
.wizard-foot-actions { display: flex; gap: 10px; }
475+
.compose-page-grid { height: 100%; min-height: 0; display: grid; grid-template-columns: minmax(0, 7fr) minmax(220px, 3fr); gap: 16px; }
476+
.compose-page-summary { min-height: 0; overflow: auto; }
477+
.compose-page-summary .source-summary { max-height: 180px; overflow: auto; }
478+
.application-wizard-page .compose-editor-shell { height: 100%; min-height: 0; }
479+
.application-wizard-page .compose-dropzone { height: calc(100% - 72px); min-height: 0; }
480+
.application-wizard-page .compose-editor { height: 100%; min-height: 0; }
481+
.application-wizard-page .cm-editor { height: 100% !important; }
482+
.wizard-confirm { position: fixed; z-index: 20; right: 24px; bottom: 24px; width: min(420px, calc(100vw - 32px)); padding: 18px; border: 1px solid #59621d; border-radius: 12px; background: #1b1e24; box-shadow: 0 18px 50px #0008; }
483+
.wizard-confirm p { color: #aeb4c0; }
484+
.wizard-confirm > div { display: flex; justify-content: flex-end; gap: 10px; }
485+
@media (min-width: 800px) {
486+
.application-wizard-page .wizard-step-content:has(.compose-editor-shell) { display: grid; grid-template-rows: auto 1fr; }
487+
.application-wizard-page .wizard-step-content:has(.compose-editor-shell) > section { min-height: 0; }
488+
}
489+
@media (max-width: 700px) {
490+
.application-wizard-page { margin: 0; height: 100%; }
491+
.application-wizard-head, .application-wizard-nav, .application-wizard-foot { padding-left: 16px; padding-right: 16px; }
492+
.application-wizard-head { align-items: flex-start; flex-direction: column; }
493+
.application-wizard-body { padding: 16px; overflow: auto; }
494+
.application-wizard-foot { position: sticky; bottom: 0; }
495+
.wizard-foot-actions { flex: 1; justify-content: flex-end; }
496+
.wizard-foot-actions .primary { min-width: 0; }
497+
.application-wizard-page .compose-editor-shell { height: 460px; }
498+
.application-wizard-page .compose-dropzone { height: calc(100% - 72px); }
499+
.compose-page-grid { height: auto; grid-template-columns: 1fr; }
500+
.compose-page-summary { overflow: visible; }
501+
}
445502
.review-section {
446503
margin: 18px 0;
447504
padding: 14px;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { mount } from "@vue/test-utils";
2+
import { createMemoryHistory, createRouter } from "vue-router";
3+
import { describe, expect, it } from "vitest";
4+
import ApplicationNew from "./ApplicationNew.vue";
5+
6+
describe("ApplicationNew", () => {
7+
it("renders as a page workspace without a modal or overlay", async () => {
8+
const router = createRouter({
9+
history: createMemoryHistory(),
10+
routes: [{ path: "/projects/:projectId/applications/new", component: ApplicationNew }],
11+
});
12+
await router.push("/projects/1/applications/new");
13+
await router.isReady();
14+
const wrapper = mount(ApplicationNew, {
15+
global: {
16+
plugins: [router],
17+
stubs: {
18+
ComposeCodeEditor: { template: "<div class='cm-editor' />" },
19+
},
20+
},
21+
});
22+
expect(wrapper.find('[role="dialog"]').exists()).toBe(false);
23+
expect(wrapper.find(".modal-backdrop").exists()).toBe(false);
24+
expect(wrapper.text()).toContain("Nova aplicação");
25+
expect(wrapper.text()).toContain("Continuar");
26+
27+
await wrapper.get('input[placeholder="Ex.: Frontend"]').setValue("Demo");
28+
await wrapper.get("button.primary").trigger("click");
29+
expect(wrapper.text()).toContain("Como esta aplicação será publicada?");
30+
});
31+
});

0 commit comments

Comments
 (0)