123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- <template>
- <div
- :class="[workflowHostKeys.length === 0 ? 'h-48' : 'h-56']"
- class="bg-accent rounded-b-2xl absolute top-0 left-0 w-full"
- ></div>
- <div
- :class="[workflowHostKeys.length === 0 ? 'mb-6' : 'mb-2']"
- class="dark placeholder-black relative z-10 text-white px-5 pt-8"
- >
- <div class="flex items-center mb-4">
- <h1 class="text-xl font-semibold text-white">Automa</h1>
- <div class="flex-grow"></div>
- <ui-button
- v-tooltip.group="t('home.record.title')"
- icon
- class="mr-2"
- @click="state.newRecordingModal = true"
- >
- <v-remixicon name="riRecordCircleLine" />
- </ui-button>
- <ui-button
- v-tooltip.group="
- t(`home.elementSelector.${state.haveAccess ? 'name' : 'noAccess'}`)
- "
- icon
- class="mr-2"
- @click="initElementSelector"
- >
- <v-remixicon name="riFocus3Line" />
- </ui-button>
- <ui-button
- v-tooltip.group="t('common.dashboard')"
- icon
- :title="t('common.dashboard')"
- @click="openDashboard"
- >
- <v-remixicon name="riHome5Line" />
- </ui-button>
- </div>
- <div class="flex">
- <ui-input
- v-model="state.query"
- :placeholder="`${t('common.search')}...`"
- prepend-icon="riSearch2Line"
- class="w-full search-input"
- />
- </div>
- <ui-tabs
- v-if="workflowHostKeys.length > 0"
- v-model="state.activeTab"
- fill
- class="mt-1"
- >
- <ui-tab v-for="type in workflowTypes" :key="type" :value="type">
- {{ t(`home.workflow.type.${type}`) }}
- </ui-tab>
- </ui-tabs>
- </div>
- <div class="px-5 pb-5 space-y-2">
- <ui-card v-if="Workflow.all().length === 0" class="text-center">
- <img src="@/assets/svg/alien.svg" />
- <p class="font-semibold">{{ t('message.empty') }}</p>
- <ui-button
- variant="accent"
- class="mt-6"
- @click="openDashboard('/workflows')"
- >
- {{ t('home.workflow.new') }}
- </ui-button>
- </ui-card>
- <home-workflow-card
- v-for="workflow in workflows"
- :key="workflow.id"
- :workflow="workflow"
- :tab="state.activeTab"
- @details="openDashboard(`/workflows/${$event.id}`)"
- @update="updateWorkflow(workflow.id, $event)"
- @execute="executeWorkflow"
- @rename="renameWorkflow"
- @delete="deleteWorkflow"
- />
- </div>
- <ui-modal v-model="state.newRecordingModal" custom-content>
- <ui-card
- :style="{ height: `${state.cardHeight}px` }"
- class="w-full recording-card overflow-hidden rounded-b-none"
- padding="p-0"
- >
- <div class="flex items-center px-4 pt-4 pb-2">
- <p class="flex-1 font-semibold">
- {{ t('home.record.title') }}
- </p>
- <v-remixicon
- class="text-gray-600 dark:text-gray-300 cursor-pointer"
- name="riCloseLine"
- size="20"
- @click="state.newRecordingModal = false"
- ></v-remixicon>
- </div>
- <home-start-recording
- @record="recordWorkflow"
- @close="state.newRecordingModal = false"
- @update="state.cardHeight = recordingCardHeight[$event] || 255"
- />
- </ui-card>
- </ui-modal>
- </template>
- <script setup>
- import { computed, onMounted, shallowReactive } from 'vue';
- import { useI18n } from 'vue-i18n';
- import { useStore } from 'vuex';
- import browser from 'webextension-polyfill';
- import { useDialog } from '@/composable/dialog';
- import { useGroupTooltip } from '@/composable/groupTooltip';
- import { sendMessage } from '@/utils/message';
- import Workflow from '@/models/workflow';
- import HomeWorkflowCard from '@/components/popup/home/HomeWorkflowCard.vue';
- import HomeStartRecording from '@/components/popup/home/HomeStartRecording.vue';
- const workflowTypes = ['local', 'host'];
- const recordingCardHeight = {
- new: 255,
- existing: 480,
- };
- const { t } = useI18n();
- const store = useStore();
- const dialog = useDialog();
- useGroupTooltip();
- const state = shallowReactive({
- query: '',
- cardHeight: 255,
- haveAccess: true,
- activeTab: 'local',
- newRecordingModal: false,
- });
- const workflowHostKeys = computed(() => Object.keys(store.state.workflowHosts));
- const workflowHosts = computed(() => {
- if (state.activeTab !== 'host') return [];
- return workflowHostKeys.value.reduce((acc, key) => {
- const workflow = store.state.workflowHosts[key];
- const isMatch = workflow.name
- .toLocaleLowerCase()
- .includes(state.query.toLocaleLowerCase());
- if (isMatch) acc.push({ ...workflow, id: key });
- return acc;
- }, []);
- });
- const localWorkflows = computed(() => {
- if (state.activeTab !== 'local') return [];
- return Workflow.query()
- .where(({ name }) =>
- name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
- )
- .orderBy('createdAt', 'desc')
- .get();
- });
- const workflows = computed(() =>
- state.activeTab === 'local' ? localWorkflows.value : workflowHosts.value
- );
- function executeWorkflow(workflow) {
- sendMessage('workflow:execute', workflow, 'background');
- }
- function updateWorkflow(id, data) {
- return Workflow.update({
- where: id,
- data,
- });
- }
- function renameWorkflow({ id, name }) {
- dialog.prompt({
- title: t('home.workflow.rename'),
- placeholder: t('common.name'),
- okText: t('common.rename'),
- inputValue: name,
- onConfirm: (newName) => {
- updateWorkflow(id, { name: newName });
- },
- });
- }
- function deleteWorkflow({ id, name }) {
- dialog.confirm({
- title: t('home.workflow.delete'),
- okVariant: 'danger',
- body: t('message.delete', { name }),
- onConfirm: () => {
- if (state.activeTab === 'local') {
- Workflow.delete(id);
- } else {
- store.commit('deleteStateNested', `workflowHosts.${id}`);
- if (workflowHostKeys.value.length === 0) {
- state.activeTab = 'local';
- }
- browser.storage.local.set({ workflowHosts: store.state.workflowHosts });
- }
- },
- });
- }
- function openDashboard(url) {
- sendMessage('open:dashboard', url, 'background');
- }
- async function initElementSelector() {
- const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
- try {
- const result = await browser.tabs.sendMessage(tab.id, {
- type: 'automa-element-selector',
- });
- if (!result) throw new Error('not-found');
- window.close();
- } catch (error) {
- if (error.message.includes('Could not establish connection.')) {
- await browser.tabs.executeScript(tab.id, {
- allFrames: true,
- file: './elementSelector.bundle.js',
- });
- initElementSelector();
- }
- console.error(error);
- }
- }
- async function recordWorkflow(options = {}) {
- try {
- const flows = [];
- const [activeTab] = await browser.tabs.query({
- active: true,
- currentWindow: true,
- });
- if (activeTab && activeTab.url.startsWith('http')) {
- flows.push({
- id: 'new-tab',
- description: activeTab.url,
- data: { url: activeTab.url },
- });
- }
- await browser.storage.local.set({
- isRecording: true,
- recording: {
- flows,
- name: 'unnamed',
- activeTab: {
- id: activeTab.id,
- url: activeTab.url,
- },
- ...options,
- },
- });
- await browser.browserAction.setBadgeBackgroundColor({ color: '#ef4444' });
- await browser.browserAction.setBadgeText({ text: 'rec' });
- const tabs = await browser.tabs.query({});
- for (const tab of tabs) {
- if (tab.url.startsWith('http')) {
- await browser.tabs.executeScript(tab.id, {
- allFrames: true,
- file: 'recordWorkflow.bundle.js',
- });
- }
- }
- window.close();
- } catch (error) {
- console.error(error);
- }
- }
- onMounted(async () => {
- const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
- state.haveAccess = /^(https?)/.test(tab.url);
- });
- </script>
- <style>
- .recording-card {
- transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1) !important;
- }
- </style>
|