feat(web): pipeline merge wizard
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,471 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type {
|
||||||
|
MergePipelineRequest,
|
||||||
|
MergePipelineResultDto,
|
||||||
|
PipelineDto,
|
||||||
|
PipelineMergePreviewDto,
|
||||||
|
PipelineMergePreviewStageDto,
|
||||||
|
} from '@mento/shared';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import { toFaDigits } from '@/lib/jalali';
|
||||||
|
import { UiButton, UiField, UiSelect } from '@/components/ui';
|
||||||
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
|
||||||
|
type Step = 'target' | 'map' | 'review';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open: boolean;
|
||||||
|
sourceId: string | null;
|
||||||
|
pipelines: PipelineDto[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
done: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const step = ref<Step>('target');
|
||||||
|
const targetId = ref('');
|
||||||
|
const preview = ref<PipelineMergePreviewDto | null>(null);
|
||||||
|
const stageMap = ref<Record<string, string>>({});
|
||||||
|
const busy = ref(false);
|
||||||
|
const error = ref('');
|
||||||
|
|
||||||
|
const sourceName = computed(() => {
|
||||||
|
if (preview.value) return preview.value.source.name;
|
||||||
|
return props.pipelines.find((pipeline) => pipeline.id === props.sourceId)?.name ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedTarget = computed(
|
||||||
|
() => preview.value?.targets.find((target) => target.id === targetId.value) ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const canContinueTarget = computed(() => Boolean(targetId.value && selectedTarget.value));
|
||||||
|
const canContinueMap = computed(() => {
|
||||||
|
const current = preview.value;
|
||||||
|
if (!current) return false;
|
||||||
|
return current.stages.every((stage) => Boolean(stageMap.value[stage.id]));
|
||||||
|
});
|
||||||
|
|
||||||
|
function apiError(e: unknown, fallback: string) {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||||
|
if (Array.isArray(msg)) return msg.join('، ');
|
||||||
|
if (msg) return msg;
|
||||||
|
if (!e.response) return 'اتصال به API برقرار نشد.';
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
step.value = 'target';
|
||||||
|
targetId.value = '';
|
||||||
|
preview.value = null;
|
||||||
|
stageMap.value = {};
|
||||||
|
busy.value = false;
|
||||||
|
error.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function compatibleTargets(src: PipelineMergePreviewStageDto) {
|
||||||
|
const target = selectedTarget.value;
|
||||||
|
if (!target) return [];
|
||||||
|
return target.stages.filter((stage) => {
|
||||||
|
if (src.isWon) return stage.isWon;
|
||||||
|
if (src.isLost) return stage.isLost;
|
||||||
|
return !stage.isWon && !stage.isLost;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(stage: Pick<PipelineMergePreviewStageDto, 'isWon' | 'isLost'>) {
|
||||||
|
if (stage.isWon) return 'برد';
|
||||||
|
if (stage.isLost) return 'باخت';
|
||||||
|
return 'باز';
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncStageMap() {
|
||||||
|
const current = preview.value;
|
||||||
|
if (!current || !targetId.value) {
|
||||||
|
stageMap.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next: Record<string, string> = {};
|
||||||
|
for (const sourceStage of current.stages) {
|
||||||
|
const compatible = compatibleTargets(sourceStage);
|
||||||
|
const currentTargetId = stageMap.value[sourceStage.id];
|
||||||
|
if (compatible.some((stage) => stage.id === currentTargetId)) {
|
||||||
|
next[sourceStage.id] = currentTargetId;
|
||||||
|
} else if (compatible.length === 1) {
|
||||||
|
next[sourceStage.id] = compatible[0].id;
|
||||||
|
} else {
|
||||||
|
next[sourceStage.id] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stageMap.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPreview(id: string) {
|
||||||
|
busy.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<PipelineMergePreviewDto>(`/pipelines/${id}/merge-preview`);
|
||||||
|
preview.value = data;
|
||||||
|
} catch (e) {
|
||||||
|
error.value = apiError(e, 'پیشنمایش ادغام ناموفق بود.');
|
||||||
|
} finally {
|
||||||
|
busy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFromTarget() {
|
||||||
|
if (!canContinueTarget.value) {
|
||||||
|
error.value = 'کاریز مقصد را انتخاب کنید.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.value = '';
|
||||||
|
syncStageMap();
|
||||||
|
step.value = 'map';
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextFromMap() {
|
||||||
|
if (!canContinueMap.value) {
|
||||||
|
error.value = 'برای همه مرحلهها مقصد سازگار انتخاب کنید.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.value = '';
|
||||||
|
step.value = 'review';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitMerge() {
|
||||||
|
if (!props.sourceId || !preview.value || !canContinueMap.value || busy.value) return;
|
||||||
|
|
||||||
|
busy.value = true;
|
||||||
|
error.value = '';
|
||||||
|
try {
|
||||||
|
const body: MergePipelineRequest = {
|
||||||
|
targetPipelineId: targetId.value,
|
||||||
|
stageMap: stageMap.value,
|
||||||
|
leadFormMode: 'retarget_all',
|
||||||
|
};
|
||||||
|
await api.post<MergePipelineResultDto>(`/pipelines/${props.sourceId}/merge`, body);
|
||||||
|
emit('done');
|
||||||
|
} catch (e) {
|
||||||
|
error.value = apiError(e, 'ادغام کاریز ناموفق بود.');
|
||||||
|
} finally {
|
||||||
|
busy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.open, props.sourceId] as const,
|
||||||
|
async ([open, id]) => {
|
||||||
|
resetState();
|
||||||
|
if (!open || !id) return;
|
||||||
|
await loadPreview(id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(targetId, () => {
|
||||||
|
error.value = '';
|
||||||
|
syncStageMap();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="merge-wizard">
|
||||||
|
<ol class="steps" aria-label="مراحل ادغام کاریز">
|
||||||
|
<li :class="{ active: step === 'target', done: step !== 'target' }">۱. مقصد</li>
|
||||||
|
<li :class="{ active: step === 'map', done: step === 'review' }">۲. نگاشت</li>
|
||||||
|
<li :class="{ active: step === 'review' }">۳. بازبینی</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p v-if="busy && !preview" class="muted">در حال آمادهسازی پیشنمایش…</p>
|
||||||
|
|
||||||
|
<template v-else-if="preview">
|
||||||
|
<section v-if="step === 'target'" class="step-panel">
|
||||||
|
<div class="intro">
|
||||||
|
<UiIcon name="layers" :size="20" />
|
||||||
|
<div>
|
||||||
|
<h3>کاریز مقصد را انتخاب کنید</h3>
|
||||||
|
<p>
|
||||||
|
معاملات و فرمهای سرنخ از «{{ preview.source.name }}» به کاریز مقصد منتقل میشوند.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiField label="کاریز مقصد" required>
|
||||||
|
<UiSelect v-model="targetId" required>
|
||||||
|
<option value="" disabled>انتخاب کنید</option>
|
||||||
|
<option v-for="target in preview.targets" :key="target.id" :value="target.id">
|
||||||
|
{{ target.name }}
|
||||||
|
</option>
|
||||||
|
</UiSelect>
|
||||||
|
</UiField>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<article>
|
||||||
|
<strong>{{ toFaDigits(preview.dealCount) }}</strong>
|
||||||
|
<span>معامله منتقل میشود</span>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<strong>{{ toFaDigits(preview.leadForms.length) }}</strong>
|
||||||
|
<span>فرم سرنخ به مقصد وصل میشود</span>
|
||||||
|
</article>
|
||||||
|
<article v-if="preview.source.isDefault" class="warn">
|
||||||
|
<strong>هشدار</strong>
|
||||||
|
<span>کاریز مبدا پیشفرض است و بعد از ادغام حذف میشود.</span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="selectedTarget?.isDefault" class="note">مقصد انتخابشده کاریز پیشفرض است.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="step === 'map'" class="step-panel">
|
||||||
|
<div class="intro">
|
||||||
|
<UiIcon name="target" :size="20" />
|
||||||
|
<div>
|
||||||
|
<h3>مرحلههای مبدا را نگاشت کنید</h3>
|
||||||
|
<p>هر مرحله فقط به مرحلهای با وضعیت سازگار در مقصد وصل میشود.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="map-list">
|
||||||
|
<article v-for="sourceStage in preview.stages" :key="sourceStage.id" class="map-row">
|
||||||
|
<div class="stage-info">
|
||||||
|
<strong>{{ sourceStage.name }}</strong>
|
||||||
|
<span>
|
||||||
|
{{ statusLabel(sourceStage) }} · {{ toFaDigits(sourceStage.dealCount) }} معامله
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<UiSelect v-model="stageMap[sourceStage.id]" :error="!stageMap[sourceStage.id]">
|
||||||
|
<option value="" disabled>مرحله مقصد</option>
|
||||||
|
<option
|
||||||
|
v-for="targetStage in compatibleTargets(sourceStage)"
|
||||||
|
:key="targetStage.id"
|
||||||
|
:value="targetStage.id"
|
||||||
|
>
|
||||||
|
{{ targetStage.name }}
|
||||||
|
</option>
|
||||||
|
</UiSelect>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else class="step-panel">
|
||||||
|
<div class="intro">
|
||||||
|
<UiIcon name="check" :size="20" />
|
||||||
|
<div>
|
||||||
|
<h3>بازبینی نهایی</h3>
|
||||||
|
<p>
|
||||||
|
«{{ toFaDigits(preview.dealCount) }}» معامله منتقل میشود.
|
||||||
|
{{ toFaDigits(preview.leadForms.length) }} فرم سرنخ به مقصد وصل میشود.
|
||||||
|
کاریز «{{ preview.source.name }}» حذف میشود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="review-list">
|
||||||
|
<div>
|
||||||
|
<dt>مبدا</dt>
|
||||||
|
<dd>{{ preview.source.name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>مقصد</dt>
|
||||||
|
<dd>{{ selectedTarget?.name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>روش فرمها</dt>
|
||||||
|
<dd>همه فرمهای سرنخ به مقصد وصل میشوند.</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<p v-else-if="!error" class="muted">
|
||||||
|
برای شروع ادغام، کاریز مبدا را انتخاب کنید.
|
||||||
|
<template v-if="sourceName">مبدا فعلی: {{ sourceName }}</template>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<UiButton variant="ghost" :disabled="busy" @click="emit('close')">بستن</UiButton>
|
||||||
|
<UiButton v-if="step === 'map'" variant="ghost" :disabled="busy" @click="step = 'target'">
|
||||||
|
قبلی
|
||||||
|
</UiButton>
|
||||||
|
<UiButton v-else-if="step === 'review'" variant="ghost" :disabled="busy" @click="step = 'map'">
|
||||||
|
قبلی
|
||||||
|
</UiButton>
|
||||||
|
<UiButton v-if="step === 'target'" :disabled="!preview || !canContinueTarget" @click="nextFromTarget">
|
||||||
|
ادامه
|
||||||
|
</UiButton>
|
||||||
|
<UiButton v-else-if="step === 'map'" :disabled="!canContinueMap" @click="nextFromMap">
|
||||||
|
بازبینی
|
||||||
|
</UiButton>
|
||||||
|
<UiButton v-else variant="danger" :loading="busy" :disabled="!canContinueMap" @click="submitMerge">
|
||||||
|
ادغام و حذف مبدا
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.merge-wizard {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps li {
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps li.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps li.done {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.65rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro h3,
|
||||||
|
.intro p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro p {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid article {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.2rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid strong {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid span,
|
||||||
|
.muted,
|
||||||
|
.note {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid .warn {
|
||||||
|
border-color: var(--danger);
|
||||||
|
background: var(--danger-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(12rem, 18rem);
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-info {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-info span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-list div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-bottom: 0.55rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-list dt,
|
||||||
|
.review-list dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-list dt {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #b91c1c;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.map-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@ import { api } from '@/lib/api';
|
|||||||
import { toFaDigits } from '@/lib/jalali';
|
import { toFaDigits } from '@/lib/jalali';
|
||||||
import { UiButton, UiCard, UiEmptyState, UiField, UiInput, UiModal, UiPageHeader } from '@/components/ui';
|
import { UiButton, UiCard, UiEmptyState, UiField, UiInput, UiModal, UiPageHeader } from '@/components/ui';
|
||||||
import UiIcon from '@/components/UiIcon.vue';
|
import UiIcon from '@/components/UiIcon.vue';
|
||||||
|
import PipelineMergeWizard from '@/components/pipeline/PipelineMergeWizard.vue';
|
||||||
import type { PipelineDto } from '@mento/shared';
|
import type { PipelineDto } from '@mento/shared';
|
||||||
|
|
||||||
type PipelineListItem = PipelineDto & {
|
type PipelineListItem = PipelineDto & {
|
||||||
@@ -21,9 +22,6 @@ const mergeOpen = ref(false);
|
|||||||
const mergeSourceId = ref<string | null>(null);
|
const mergeSourceId = ref<string | null>(null);
|
||||||
|
|
||||||
const canMerge = computed(() => pipelines.value.length >= 2);
|
const canMerge = computed(() => pipelines.value.length >= 2);
|
||||||
const mergeSource = computed(() =>
|
|
||||||
pipelines.value.find((pipeline) => pipeline.id === mergeSourceId.value) ?? null,
|
|
||||||
);
|
|
||||||
|
|
||||||
function apiError(e: unknown, fallback: string) {
|
function apiError(e: unknown, fallback: string) {
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
@@ -77,6 +75,13 @@ function openMerge(pipeline: PipelineListItem) {
|
|||||||
|
|
||||||
function closeMerge() {
|
function closeMerge() {
|
||||||
mergeOpen.value = false;
|
mergeOpen.value = false;
|
||||||
|
mergeSourceId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMergeDone() {
|
||||||
|
okMsg.value = 'کاریز با موفقیت ادغام شد.';
|
||||||
|
await load();
|
||||||
|
closeMerge();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
@@ -149,16 +154,13 @@ onMounted(load);
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<UiModal :open="mergeOpen" title="ادغام کاریز" @close="closeMerge">
|
<UiModal :open="mergeOpen" title="ادغام کاریز" @close="closeMerge">
|
||||||
<!-- TODO(Task 4): mount PipelineMergeWizard here with mergeSourceId and close events. -->
|
<PipelineMergeWizard
|
||||||
<p class="modal-copy">
|
:open="mergeOpen"
|
||||||
ویزارد ادغام در مرحله بعد اضافه میشود.
|
:source-id="mergeSourceId"
|
||||||
</p>
|
:pipelines="pipelines"
|
||||||
<p v-if="mergeSource" class="muted">
|
@close="closeMerge"
|
||||||
کاریز مبدا: {{ mergeSource.name }}
|
@done="onMergeDone"
|
||||||
</p>
|
/>
|
||||||
<template #footer>
|
|
||||||
<UiButton variant="ghost" @click="closeMerge">بستن</UiButton>
|
|
||||||
</template>
|
|
||||||
</UiModal>
|
</UiModal>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -241,10 +243,6 @@ onMounted(load);
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-copy {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ok {
|
.ok {
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user