- Added new automation-related routes and controllers for managing automations. - Introduced automation nodes in the UI with distinct styles and interactions. - Updated sidebar to include navigation for automations. - Enhanced post creation logic to support automation metadata. - Refactored content type and platform enums into types for better type safety. - Added localization for automation-related terms in English, Spanish, and Portuguese. - Improved error handling in various components to accommodate new features.
202 lines
9.1 KiB
Vue
202 lines
9.1 KiB
Vue
<script setup lang="ts">
|
|
import { IconAlertTriangle, IconChevronDown, IconChevronUp } from '@tabler/icons-vue';
|
|
import { computed, ref } from 'vue';
|
|
|
|
import InputError from '@/components/InputError.vue';
|
|
import { Avatar } from '@/components/ui/avatar';
|
|
import {
|
|
Combobox,
|
|
ComboboxAnchor,
|
|
ComboboxEmpty,
|
|
ComboboxGroup,
|
|
ComboboxInput,
|
|
ComboboxItem,
|
|
ComboboxList,
|
|
ComboboxTrigger,
|
|
} from '@/components/ui/combobox';
|
|
import { getMediaValidationWarning } from '@/composables/useMedia';
|
|
import { usePageErrors } from '@/composables/usePageErrors';
|
|
import { getPlatformLogo } from '@/composables/usePlatformLogo';
|
|
import { ContentType } from '@/types/content-type';
|
|
import type { PinterestBoard } from '@/types';
|
|
import type { MediaItem } from '@/types/media';
|
|
|
|
interface SocialAccount {
|
|
id: string;
|
|
platform: string;
|
|
display_name: string;
|
|
username: string;
|
|
avatar_url: string | null;
|
|
}
|
|
|
|
interface BoardOption {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
interface Props {
|
|
socialAccount: SocialAccount | null;
|
|
contentType: string;
|
|
media: MediaItem[];
|
|
boards: PinterestBoard[];
|
|
meta: Record<string, any>;
|
|
disabled?: boolean;
|
|
previewOnly?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
disabled: false,
|
|
previewOnly: false,
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
'update:contentType': [value: string];
|
|
'update:meta': [value: Record<string, any>];
|
|
}>();
|
|
|
|
const open = ref(false);
|
|
|
|
const variants = [
|
|
{ value: ContentType.PinterestPin, labelKey: 'posts.form.pinterest.variant.pin' },
|
|
{ value: ContentType.PinterestVideoPin, labelKey: 'posts.form.pinterest.variant.video_pin' },
|
|
{ value: ContentType.PinterestCarousel, labelKey: 'posts.form.pinterest.variant.carousel' },
|
|
];
|
|
|
|
const pickVariant = (value: string) => {
|
|
if (props.disabled) return;
|
|
emit('update:contentType', value);
|
|
};
|
|
|
|
const warning = computed(() => getMediaValidationWarning(props.contentType, props.media));
|
|
|
|
const boardOptions = computed<BoardOption[]>(() =>
|
|
props.boards.map((b) => ({ value: b.id, label: b.name })),
|
|
);
|
|
|
|
const selectedBoard = computed<BoardOption | undefined>({
|
|
get: () => boardOptions.value.find((b) => b.value === props.meta?.board_id),
|
|
set: (board) => emit('update:meta', { ...props.meta, board_id: board?.value ?? null }),
|
|
});
|
|
|
|
// Surface the backend validation error keyed by platform index
|
|
// (`platforms.0.meta.board_id`). Suffix match avoids threading the index
|
|
// through props. Cleared as soon as a board is picked locally so the user
|
|
// doesn't see a stale error after fixing the issue.
|
|
const errors = usePageErrors();
|
|
const boardError = computed<string | undefined>(() => {
|
|
if (props.meta?.board_id) return undefined;
|
|
return Object.entries(errors.value).find(([key]) => key.endsWith('.meta.board_id'))?.[1];
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rounded-xl border-2 border-foreground bg-card shadow-2xs">
|
|
<button
|
|
type="button"
|
|
class="flex w-full cursor-pointer items-center justify-between gap-3 p-4 text-sm"
|
|
@click="open = !open"
|
|
>
|
|
<span class="flex min-w-0 items-center gap-2">
|
|
<span class="inline-flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-foreground bg-card shadow-2xs">
|
|
<img :src="getPlatformLogo('pinterest')" alt="Pinterest" class="size-full object-cover" />
|
|
</span>
|
|
<span class="truncate font-bold text-foreground">{{ $t('posts.form.pinterest.settings') }}</span>
|
|
<span v-if="socialAccount?.username" class="truncate font-medium text-foreground/60">· @{{ socialAccount.username }}</span>
|
|
</span>
|
|
<IconChevronUp v-if="open" class="size-4 shrink-0 text-foreground/60" />
|
|
<IconChevronDown v-else class="size-4 shrink-0 text-foreground/60" />
|
|
</button>
|
|
|
|
<div v-if="open" class="space-y-5 border-t-2 border-foreground/10 px-4 pb-4 pt-4">
|
|
<div v-if="socialAccount" class="flex items-center gap-3 rounded-lg bg-foreground/5 p-3">
|
|
<Avatar
|
|
:src="socialAccount.avatar_url"
|
|
:name="socialAccount.display_name"
|
|
class="size-9 shrink-0 rounded-full border-2 border-foreground shadow-2xs"
|
|
/>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-[11px] font-black uppercase tracking-widest text-foreground/60">{{ $t('posts.form.pinterest.posting_to') }}</p>
|
|
<p class="truncate text-sm">
|
|
<span class="font-bold text-foreground">{{ socialAccount.display_name }}</span>
|
|
<span v-if="socialAccount?.username" class="font-medium text-foreground/60"> @{{ socialAccount.username }}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<p class="text-[11px] font-black uppercase tracking-widest text-foreground/60">{{ $t('posts.form.pinterest.variant_label') }}</p>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
v-for="variant in variants"
|
|
:key="variant.value"
|
|
type="button"
|
|
class="cursor-pointer rounded-full border-2 px-3 py-1 text-xs font-bold uppercase tracking-widest transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
:class="contentType === variant.value
|
|
? 'border-foreground bg-violet-100 text-foreground shadow-2xs'
|
|
: 'border-foreground/30 text-foreground/70 hover:border-foreground hover:text-foreground'"
|
|
:disabled="disabled"
|
|
@click="pickVariant(variant.value)"
|
|
>
|
|
{{ $t(variant.labelKey) }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<p class="text-[11px] font-black uppercase tracking-widest text-foreground/60">{{ $t('posts.form.pinterest.board') }}</p>
|
|
<p
|
|
v-if="boards.length === 0"
|
|
class="flex items-start gap-2 rounded-lg border-2 border-foreground/30 bg-foreground/5 p-2 text-xs font-semibold text-foreground/60"
|
|
>
|
|
<IconAlertTriangle class="mt-0.5 size-3.5 shrink-0" />
|
|
{{ $t('posts.form.pinterest.no_boards') }}
|
|
</p>
|
|
<template v-else>
|
|
<Combobox
|
|
v-model="selectedBoard"
|
|
:display-value="(b: BoardOption | undefined) => b?.label ?? ''"
|
|
:disabled="disabled"
|
|
>
|
|
<ComboboxAnchor class="w-full">
|
|
<ComboboxTrigger as-child>
|
|
<button
|
|
type="button"
|
|
class="flex w-full items-center justify-between rounded-lg border-2 bg-card px-3 py-2 text-sm font-medium text-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
:class="boardError ? 'border-rose-500' : 'border-foreground/30 hover:border-foreground'"
|
|
:disabled="disabled"
|
|
>
|
|
<span :class="selectedBoard ? 'text-foreground' : 'text-foreground/50'">
|
|
{{ selectedBoard ? selectedBoard.label : $t('posts.form.pinterest.select_board') }}
|
|
</span>
|
|
<IconChevronDown class="size-4 shrink-0 text-foreground/60" />
|
|
</button>
|
|
</ComboboxTrigger>
|
|
</ComboboxAnchor>
|
|
<ComboboxList>
|
|
<ComboboxInput :placeholder="$t('posts.form.pinterest.search_board')" />
|
|
<ComboboxEmpty>{{ $t('posts.form.pinterest.no_board_found') }}</ComboboxEmpty>
|
|
<ComboboxGroup>
|
|
<ComboboxItem
|
|
v-for="board in boardOptions"
|
|
:key="board.value"
|
|
:value="board"
|
|
>
|
|
{{ board.label }}
|
|
</ComboboxItem>
|
|
</ComboboxGroup>
|
|
</ComboboxList>
|
|
</Combobox>
|
|
<InputError :message="boardError" />
|
|
</template>
|
|
</div>
|
|
|
|
<p
|
|
v-if="warning && !previewOnly"
|
|
class="flex items-start gap-2 rounded-lg border-2 border-foreground bg-rose-50 p-2 text-xs font-semibold text-rose-700"
|
|
>
|
|
<IconAlertTriangle class="mt-0.5 size-3.5 shrink-0" />
|
|
{{ $t(`posts.form.warnings.${warning.key}`, warning.params) }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|