refactor(frontend): split documents and review into workspace components
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
import { nextTick, onBeforeUnmount, onMounted } from "vue";
|
||||
import CrmAuthLoading from "~~/app/components/workspace/auth/CrmAuthLoading.vue";
|
||||
import CrmAuthLoginForm from "~~/app/components/workspace/auth/CrmAuthLoginForm.vue";
|
||||
import CrmDocumentsPanel from "~~/app/components/workspace/documents/CrmDocumentsPanel.vue";
|
||||
import CrmWorkspaceTopbar from "~~/app/components/workspace/header/CrmWorkspaceTopbar.vue";
|
||||
import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
|
||||
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
||||
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
|
||||
import dashboardQuery from "~~/graphql/operations/dashboard.graphql?raw";
|
||||
@@ -3318,6 +3320,11 @@ watchEffect(() => {
|
||||
|
||||
const selectedDocument = computed(() => documents.value.find((item) => item.id === selectedDocumentId.value));
|
||||
|
||||
function updateSelectedDocumentBody(value: string) {
|
||||
if (!selectedDocument.value) return;
|
||||
selectedDocument.value.body = value;
|
||||
}
|
||||
|
||||
function openDocumentsTab(push = false) {
|
||||
selectedTab.value = "documents";
|
||||
focusedCalendarEventId.value = "";
|
||||
@@ -6323,189 +6330,45 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="selectedTab === 'documents'" class="flex h-full min-h-0 flex-col gap-0">
|
||||
<div class="grid h-full min-h-0 flex-1 gap-0 md:grid-cols-[248px_minmax(0,1fr)]">
|
||||
<aside class="h-full min-h-0 border-r border-base-300 flex flex-col">
|
||||
<div class="sticky top-0 z-20 border-b border-base-300 bg-base-100 p-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
v-model="documentSearch"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="Search documents"
|
||||
>
|
||||
|
||||
<div class="dropdown dropdown-end">
|
||||
<button
|
||||
tabindex="0"
|
||||
class="btn btn-ghost btn-sm btn-square"
|
||||
title="Sort documents"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
||||
<path d="M3 5h18v2H3zm3 6h12v2H6zm4 6h4v2h-4z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div tabindex="0" class="dropdown-content z-20 mt-2 w-44 rounded-xl border border-base-300 bg-base-100 p-2 shadow-lg">
|
||||
<p class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-base-content/55">Sort docs</p>
|
||||
<button
|
||||
v-for="option in documentSortOptions"
|
||||
:key="`document-sort-${option.value}`"
|
||||
class="btn btn-ghost btn-sm w-full justify-between"
|
||||
@click="documentSortMode = option.value"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="documentSortMode === option.value">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto p-0">
|
||||
<button
|
||||
v-for="doc in filteredDocuments"
|
||||
:key="doc.id"
|
||||
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
||||
:class="selectedDocumentId === doc.id ? 'bg-primary/10' : ''"
|
||||
@click="selectedDocumentId = doc.id"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
||||
</div>
|
||||
<p class="mt-0.5 truncate text-[11px] text-base-content/75">{{ formatDocumentScope(doc.scope) }}</p>
|
||||
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
|
||||
<p class="mt-1 text-[10px] text-base-content/55">Updated {{ formatStamp(doc.updatedAt) }}</p>
|
||||
</button>
|
||||
<p v-if="filteredDocuments.length === 0" class="px-2 py-2 text-xs text-base-content/55">
|
||||
No documents found.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<article class="h-full min-h-0 flex flex-col">
|
||||
<div v-if="selectedDocument" class="flex h-full min-h-0 flex-col p-3 md:p-4">
|
||||
<div class="border-b border-base-300 pb-2">
|
||||
<p class="font-medium">{{ selectedDocument.title }}</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
{{ formatDocumentScope(selectedDocument.scope) }} · {{ selectedDocument.owner }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-base-content/80">{{ selectedDocument.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
|
||||
<ContactCollaborativeEditor
|
||||
:key="`doc-editor-${selectedDocument.id}`"
|
||||
v-model="selectedDocument.body"
|
||||
:room="`crm-doc-${selectedDocument.id}`"
|
||||
placeholder="Describe policy, steps, rules, and exceptions..."
|
||||
<CrmDocumentsPanel
|
||||
v-else-if="selectedTab === 'documents'"
|
||||
:document-search="documentSearch"
|
||||
:document-sort-mode="documentSortMode"
|
||||
:document-sort-options="documentSortOptions"
|
||||
:filtered-documents="filteredDocuments"
|
||||
:selected-document-id="selectedDocumentId"
|
||||
:selected-document="selectedDocument"
|
||||
:format-document-scope="formatDocumentScope"
|
||||
:format-stamp="formatStamp"
|
||||
@update:document-search="documentSearch = $event"
|
||||
@update:document-sort-mode="documentSortMode = $event"
|
||||
@select-document="selectedDocumentId = $event"
|
||||
@update-selected-document-body="updateSelectedDocumentBody"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex h-full items-center justify-center text-sm text-base-content/60">
|
||||
No document selected.
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="reviewActive && selectedTab === 'communications'"
|
||||
class="pointer-events-none fixed inset-x-2 bottom-2 z-40 md:inset-auto md:right-4 md:bottom-4 md:w-[390px]"
|
||||
>
|
||||
<section class="pointer-events-auto rounded-2xl border border-base-300 bg-base-100/95 p-3 shadow-2xl backdrop-blur">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wide text-base-content/60">
|
||||
Review {{ activeChangeStepNumber }}/{{ activeChangeItems.length }}
|
||||
</p>
|
||||
<p class="truncate text-sm font-semibold text-base-content">
|
||||
{{ activeChangeItem?.title || "Change step" }}
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-xs" @click="finishReview(true)">Close</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeChangeItem" class="mt-2 rounded-xl border border-base-300 bg-base-200/35 p-2">
|
||||
<p class="text-xs text-base-content/80">
|
||||
{{ describeChangeEntity(activeChangeItem.entity) }} {{ describeChangeAction(activeChangeItem.action) }}
|
||||
</p>
|
||||
<label class="mt-1 inline-flex items-center gap-2 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs"
|
||||
:checked="activeChangeApproved"
|
||||
:disabled="activeChangeItem.rolledBack"
|
||||
@change="onActiveReviewApprovalInput"
|
||||
>
|
||||
<span>{{ activeChangeItem.rolledBack ? "Already rolled back" : "Approve this step" }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 max-h-40 space-y-1 overflow-y-auto pr-1">
|
||||
<div
|
||||
v-for="(item, index) in activeChangeItems"
|
||||
:key="`review-step-${item.id}`"
|
||||
class="flex items-center gap-2 rounded-lg border px-2 py-1"
|
||||
:class="index === activeChangeIndex ? 'border-primary/45 bg-primary/10' : 'border-base-300 bg-base-100'"
|
||||
>
|
||||
<button
|
||||
class="min-w-0 flex-1 text-left"
|
||||
@click="openChangeItemTarget(item)"
|
||||
>
|
||||
<p class="truncate text-xs font-medium text-base-content">
|
||||
{{ index + 1 }}. {{ item.title }}
|
||||
</p>
|
||||
<p class="truncate text-[11px] text-base-content/65">
|
||||
{{ describeChangeEntity(item.entity) }}
|
||||
</p>
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs"
|
||||
:checked="isReviewItemApproved(item)"
|
||||
:disabled="item.rolledBack"
|
||||
@change="onReviewItemApprovalInput(item.id, $event)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between gap-2">
|
||||
<div class="join">
|
||||
<button
|
||||
class="btn btn-xs join-item"
|
||||
:disabled="activeChangeIndex <= 0"
|
||||
@click="goToPreviousChangeStep"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs join-item"
|
||||
:disabled="activeChangeIndex >= activeChangeItems.length - 1"
|
||||
@click="goToNextChangeStep"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-[11px] text-base-content/70">
|
||||
Rollback marked: {{ selectedRollbackCount }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button class="btn btn-xs btn-outline" @click="setReviewApprovalForAll(true)">Approve all</button>
|
||||
<button class="btn btn-xs btn-outline" @click="setReviewApprovalForAll(false)">Mark all rollback</button>
|
||||
<button
|
||||
class="btn btn-xs btn-warning"
|
||||
:disabled="changeActionBusy || selectedRollbackCount === 0"
|
||||
@click="rollbackSelectedChangeItems"
|
||||
>
|
||||
{{ changeActionBusy ? "Applying..." : "Rollback selected" }}
|
||||
</button>
|
||||
<button class="btn btn-xs btn-primary ml-auto" @click="finishReview(true)">Done</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<CrmChangeReviewOverlay
|
||||
:visible="reviewActive && selectedTab === 'communications'"
|
||||
:active-change-step-number="activeChangeStepNumber"
|
||||
:active-change-items="activeChangeItems"
|
||||
:active-change-item="activeChangeItem"
|
||||
:active-change-approved="activeChangeApproved"
|
||||
:active-change-index="activeChangeIndex"
|
||||
:selected-rollback-count="selectedRollbackCount"
|
||||
:change-action-busy="changeActionBusy"
|
||||
:describe-change-entity="describeChangeEntity"
|
||||
:describe-change-action="describeChangeAction"
|
||||
:is-review-item-approved="isReviewItemApproved"
|
||||
@close="finishReview(true)"
|
||||
@active-approval-change="onActiveReviewApprovalInput"
|
||||
@item-approval-change="onReviewItemApprovalInput($event.itemId, $event.event)"
|
||||
@open-item-target="openChangeItemTarget"
|
||||
@prev-step="goToPreviousChangeStep"
|
||||
@next-step="goToNextChangeStep"
|
||||
@approve-all="setReviewApprovalForAll(true)"
|
||||
@mark-all-rollback="setReviewApprovalForAll(false)"
|
||||
@rollback-selected="rollbackSelectedChangeItems"
|
||||
@done="finishReview(true)"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import ContactCollaborativeEditor from "~~/app/components/ContactCollaborativeEditor.client.vue";
|
||||
|
||||
type DocumentSortOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type DocumentListItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
scope: string;
|
||||
summary: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type SelectedDocument = {
|
||||
id: string;
|
||||
title: string;
|
||||
scope: string;
|
||||
owner: string;
|
||||
summary: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
documentSearch: string;
|
||||
documentSortMode: string;
|
||||
documentSortOptions: DocumentSortOption[];
|
||||
filteredDocuments: DocumentListItem[];
|
||||
selectedDocumentId: string;
|
||||
selectedDocument: SelectedDocument | null;
|
||||
formatDocumentScope: (scope: string) => string;
|
||||
formatStamp: (iso: string) => string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:documentSearch", value: string): void;
|
||||
(e: "update:documentSortMode", value: string): void;
|
||||
(e: "select-document", documentId: string): void;
|
||||
(e: "update-selected-document-body", value: string): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex h-full min-h-0 flex-col gap-0">
|
||||
<div class="grid h-full min-h-0 flex-1 gap-0 md:grid-cols-[248px_minmax(0,1fr)]">
|
||||
<aside class="h-full min-h-0 border-r border-base-300 flex flex-col">
|
||||
<div class="sticky top-0 z-20 border-b border-base-300 bg-base-100 p-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
:value="props.documentSearch"
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="Search documents"
|
||||
@input="emit('update:documentSearch', ($event.target as HTMLInputElement).value)"
|
||||
>
|
||||
|
||||
<div class="dropdown dropdown-end">
|
||||
<button
|
||||
tabindex="0"
|
||||
class="btn btn-ghost btn-sm btn-square"
|
||||
title="Sort documents"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" class="h-4 w-4 fill-current">
|
||||
<path d="M3 5h18v2H3zm3 6h12v2H6zm4 6h4v2h-4z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div tabindex="0" class="dropdown-content z-20 mt-2 w-44 rounded-xl border border-base-300 bg-base-100 p-2 shadow-lg">
|
||||
<p class="px-2 pb-1 text-[11px] font-semibold uppercase tracking-wide text-base-content/55">Sort docs</p>
|
||||
<button
|
||||
v-for="option in props.documentSortOptions"
|
||||
:key="`document-sort-${option.value}`"
|
||||
class="btn btn-ghost btn-sm w-full justify-between"
|
||||
@click="emit('update:documentSortMode', option.value)"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="props.documentSortMode === option.value">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto p-0">
|
||||
<button
|
||||
v-for="doc in props.filteredDocuments"
|
||||
:key="doc.id"
|
||||
class="w-full border-b border-base-300 px-3 py-2 text-left transition hover:bg-base-200/40"
|
||||
:class="props.selectedDocumentId === doc.id ? 'bg-primary/10' : ''"
|
||||
@click="emit('select-document', doc.id)"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="min-w-0 flex-1 truncate text-xs font-semibold">{{ doc.title }}</p>
|
||||
</div>
|
||||
<p class="mt-0.5 truncate text-[11px] text-base-content/75">{{ props.formatDocumentScope(doc.scope) }}</p>
|
||||
<p class="mt-0.5 line-clamp-2 text-[11px] text-base-content/70">{{ doc.summary }}</p>
|
||||
<p class="mt-1 text-[10px] text-base-content/55">Updated {{ props.formatStamp(doc.updatedAt) }}</p>
|
||||
</button>
|
||||
<p v-if="props.filteredDocuments.length === 0" class="px-2 py-2 text-xs text-base-content/55">
|
||||
No documents found.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<article class="h-full min-h-0 flex flex-col">
|
||||
<div v-if="props.selectedDocument" class="flex h-full min-h-0 flex-col p-3 md:p-4">
|
||||
<div class="border-b border-base-300 pb-2">
|
||||
<p class="font-medium">{{ props.selectedDocument.title }}</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
{{ props.formatDocumentScope(props.selectedDocument.scope) }} · {{ props.selectedDocument.owner }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-base-content/80">{{ props.selectedDocument.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 min-h-0 flex-1 overflow-y-auto">
|
||||
<ContactCollaborativeEditor
|
||||
:key="`doc-editor-${props.selectedDocument.id}`"
|
||||
:model-value="props.selectedDocument.body"
|
||||
:room="`crm-doc-${props.selectedDocument.id}`"
|
||||
placeholder="Describe policy, steps, rules, and exceptions..."
|
||||
@update:model-value="emit('update-selected-document-body', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex h-full items-center justify-center text-sm text-base-content/60">
|
||||
No document selected.
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
type ChangeItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
entity: string;
|
||||
action: string;
|
||||
rolledBack?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
activeChangeStepNumber: number;
|
||||
activeChangeItems: ChangeItem[];
|
||||
activeChangeItem: ChangeItem | null;
|
||||
activeChangeApproved: boolean;
|
||||
activeChangeIndex: number;
|
||||
selectedRollbackCount: number;
|
||||
changeActionBusy: boolean;
|
||||
describeChangeEntity: (entity: string) => string;
|
||||
describeChangeAction: (action: string) => string;
|
||||
isReviewItemApproved: (item: ChangeItem | null | undefined) => boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void;
|
||||
(e: "active-approval-change", event: Event): void;
|
||||
(e: "item-approval-change", payload: { itemId: string; event: Event }): void;
|
||||
(e: "open-item-target", item: ChangeItem): void;
|
||||
(e: "prev-step"): void;
|
||||
(e: "next-step"): void;
|
||||
(e: "approve-all"): void;
|
||||
(e: "mark-all-rollback"): void;
|
||||
(e: "rollback-selected"): void;
|
||||
(e: "done"): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="props.visible"
|
||||
class="pointer-events-none fixed inset-x-2 bottom-2 z-40 md:inset-auto md:right-4 md:bottom-4 md:w-[390px]"
|
||||
>
|
||||
<section class="pointer-events-auto rounded-2xl border border-base-300 bg-base-100/95 p-3 shadow-2xl backdrop-blur">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wide text-base-content/60">
|
||||
Review {{ props.activeChangeStepNumber }}/{{ props.activeChangeItems.length }}
|
||||
</p>
|
||||
<p class="truncate text-sm font-semibold text-base-content">
|
||||
{{ props.activeChangeItem?.title || "Change step" }}
|
||||
</p>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-xs" @click="emit('close')">Close</button>
|
||||
</div>
|
||||
|
||||
<div v-if="props.activeChangeItem" class="mt-2 rounded-xl border border-base-300 bg-base-200/35 p-2">
|
||||
<p class="text-xs text-base-content/80">
|
||||
{{ props.describeChangeEntity(props.activeChangeItem.entity) }}
|
||||
{{ props.describeChangeAction(props.activeChangeItem.action) }}
|
||||
</p>
|
||||
<label class="mt-1 inline-flex items-center gap-2 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs"
|
||||
:checked="props.activeChangeApproved"
|
||||
:disabled="props.activeChangeItem.rolledBack"
|
||||
@change="emit('active-approval-change', $event)"
|
||||
>
|
||||
<span>{{ props.activeChangeItem.rolledBack ? "Already rolled back" : "Approve this step" }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 max-h-40 space-y-1 overflow-y-auto pr-1">
|
||||
<div
|
||||
v-for="(item, index) in props.activeChangeItems"
|
||||
:key="`review-step-${item.id}`"
|
||||
class="flex items-center gap-2 rounded-lg border px-2 py-1"
|
||||
:class="index === props.activeChangeIndex ? 'border-primary/45 bg-primary/10' : 'border-base-300 bg-base-100'"
|
||||
>
|
||||
<button
|
||||
class="min-w-0 flex-1 text-left"
|
||||
@click="emit('open-item-target', item)"
|
||||
>
|
||||
<p class="truncate text-xs font-medium text-base-content">
|
||||
{{ index + 1 }}. {{ item.title }}
|
||||
</p>
|
||||
<p class="truncate text-[11px] text-base-content/65">
|
||||
{{ props.describeChangeEntity(item.entity) }}
|
||||
</p>
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs"
|
||||
:checked="props.isReviewItemApproved(item)"
|
||||
:disabled="item.rolledBack"
|
||||
@change="emit('item-approval-change', { itemId: item.id, event: $event })"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-between gap-2">
|
||||
<div class="join">
|
||||
<button
|
||||
class="btn btn-xs join-item"
|
||||
:disabled="props.activeChangeIndex <= 0"
|
||||
@click="emit('prev-step')"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs join-item"
|
||||
:disabled="props.activeChangeIndex >= props.activeChangeItems.length - 1"
|
||||
@click="emit('next-step')"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-[11px] text-base-content/70">
|
||||
Rollback marked: {{ props.selectedRollbackCount }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button class="btn btn-xs btn-outline" @click="emit('approve-all')">Approve all</button>
|
||||
<button class="btn btn-xs btn-outline" @click="emit('mark-all-rollback')">Mark all rollback</button>
|
||||
<button
|
||||
class="btn btn-xs btn-warning"
|
||||
:disabled="props.changeActionBusy || props.selectedRollbackCount === 0"
|
||||
@click="emit('rollback-selected')"
|
||||
>
|
||||
{{ props.changeActionBusy ? "Applying..." : "Rollback selected" }}
|
||||
</button>
|
||||
<button class="btn btn-xs btn-primary ml-auto" @click="emit('done')">Done</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user