mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-11-03 18:53:17 -05:00 
			
		
		
		
	feature/finish-recipe-assets (#384)
* add features to readme
* Copy markdown reference
* prop as whole recipe
* parameter as url instead of query
* add card styling to editor
* move images to /recipes/{slug}/images
* add image to breaking changes
* fix delete and import errors
* fix debug/about response
* logger updates
* dashboard ui
* add server side events
* unorganized routes
* default slot
* add backup viewer to dashboard
* format
* add dialog to backup imports
* initial event support
* delete assets when removed
Co-authored-by: hay-kot <hay-kot@pm.me>
			
			
This commit is contained in:
		@@ -22,19 +22,26 @@
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
      <v-card-actions>
 | 
			
		||||
        <v-spacer></v-spacer>
 | 
			
		||||
        <TheDownloadBtn :button-text="$t('about.download-recipe-json')" download-url="/api/debug/last-recipe-json" />
 | 
			
		||||
        <TheDownloadBtn :button-text="$t('about.download-log')" download-url="/api/debug/log" />
 | 
			
		||||
        <TheDownloadBtn download-url="/api/debug/last-recipe-json">
 | 
			
		||||
          <template v-slot:default="{ downloadFile }">
 | 
			
		||||
            <v-btn color="primary" @click="downloadFile">
 | 
			
		||||
              <v-icon left> mdi-code-braces </v-icon> {{ $t("about.download-recipe-json") }}
 | 
			
		||||
            </v-btn>
 | 
			
		||||
          </template>
 | 
			
		||||
        </TheDownloadBtn>
 | 
			
		||||
      </v-card-actions>
 | 
			
		||||
      <v-divider></v-divider>
 | 
			
		||||
    </v-card>
 | 
			
		||||
    <LogCard />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { api } from "@/api";
 | 
			
		||||
import TheDownloadBtn from "@/components/UI/Buttons/TheDownloadBtn";
 | 
			
		||||
import LogCard from "@/components/UI/LogCard.vue";
 | 
			
		||||
export default {
 | 
			
		||||
  components: { TheDownloadBtn },
 | 
			
		||||
  components: { TheDownloadBtn, LogCard },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      prettyInfo: [],
 | 
			
		||||
@@ -79,9 +86,9 @@ export default {
 | 
			
		||||
          value: debugInfo.dbType,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: this.$t("about.sqlite-file"),
 | 
			
		||||
          name: this.$t("about.database-url"),
 | 
			
		||||
          icon: "mdi-file-cabinet",
 | 
			
		||||
          value: debugInfo.sqliteFile,
 | 
			
		||||
          value: debugInfo.dbUrl,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: this.$t("about.default-group"),
 | 
			
		||||
@@ -93,5 +100,3 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@
 | 
			
		||||
                <div class="text-truncate">
 | 
			
		||||
                  <strong>{{ backup.name }}</strong>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="text-truncate">{{ $d(new Date(backup.date), "medium") }}</div>
 | 
			
		||||
                <div class="text-truncate">{{ $d(Date.parse(backup.date), "medium") }}</div>
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-card-text>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
          </v-toolbar-items>
 | 
			
		||||
        </v-toolbar>
 | 
			
		||||
        <v-card-title> {{ name }} </v-card-title>
 | 
			
		||||
        <v-card-subtitle class="mb-n3"> {{ $d(new Date(date), "medium") }} </v-card-subtitle>
 | 
			
		||||
        <v-card-subtitle class="mb-n3" v-if="date"> {{ $d(new Date(date), "medium") }} </v-card-subtitle>
 | 
			
		||||
        <v-divider></v-divider>
 | 
			
		||||
 | 
			
		||||
        <v-card-text>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								frontend/src/pages/Admin/Dashboard/BackupViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								frontend/src/pages/Admin/Dashboard/BackupViewer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <ImportSummaryDialog ref="report" />
 | 
			
		||||
    <ImportDialog
 | 
			
		||||
      :name="selectedName"
 | 
			
		||||
      :date="selectedDate"
 | 
			
		||||
      ref="import_dialog"
 | 
			
		||||
      @import="importBackup"
 | 
			
		||||
      @delete="deleteBackup"
 | 
			
		||||
    />
 | 
			
		||||
    <StatCard icon="mdi-backup-restore" :color="color">
 | 
			
		||||
      <template v-slot:after-heading>
 | 
			
		||||
        <div class="ml-auto text-right">
 | 
			
		||||
          <div class="body-3 grey--text font-weight-light" v-text="'Backups'" />
 | 
			
		||||
 | 
			
		||||
          <h3 class="display-2 font-weight-light text--primary">
 | 
			
		||||
            <small> {{ total }}</small>
 | 
			
		||||
          </h3>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
      <div class="d-flex row py-3 justify-end">
 | 
			
		||||
        <TheUploadBtn url="/api/backups/upload" @uploaded="getAvailableBackups">
 | 
			
		||||
          <template v-slot="{ isSelecting, onButtonClick }">
 | 
			
		||||
            <v-btn :loading="isSelecting" class="mx-2" small :color="color" @click="onButtonClick">
 | 
			
		||||
              <v-icon left> mdi-cloud-upload </v-icon> Upload
 | 
			
		||||
            </v-btn>
 | 
			
		||||
          </template>
 | 
			
		||||
        </TheUploadBtn>
 | 
			
		||||
        <v-btn :loading="loading" class="mx-2" small :color="color" @click="createBackup">
 | 
			
		||||
          <v-icon left> mdi-plus </v-icon> Create
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
      <template v-slot:bottom>
 | 
			
		||||
        <v-virtual-scroll height="290" item-height="70" :items="availableBackups">
 | 
			
		||||
          <template v-slot:default="{ item }">
 | 
			
		||||
            <v-list-item @click.prevent="openDialog(item)">
 | 
			
		||||
              <v-list-item-avatar>
 | 
			
		||||
                <v-icon large dark :color="color">
 | 
			
		||||
                  mdi-backup-restore
 | 
			
		||||
                </v-icon>
 | 
			
		||||
              </v-list-item-avatar>
 | 
			
		||||
 | 
			
		||||
              <v-list-item-content>
 | 
			
		||||
                <v-list-item-title v-text="item.name"></v-list-item-title>
 | 
			
		||||
 | 
			
		||||
                <v-list-item-subtitle>
 | 
			
		||||
                  {{ $d(Date.parse(item.date), "medium") }}
 | 
			
		||||
                </v-list-item-subtitle>
 | 
			
		||||
              </v-list-item-content>
 | 
			
		||||
 | 
			
		||||
              <v-list-item-action class="ml-auto">
 | 
			
		||||
                <v-btn large icon @click.stop="deleteBackup(item.name)">
 | 
			
		||||
                  <v-icon color="error">mdi-delete</v-icon>
 | 
			
		||||
                </v-btn>
 | 
			
		||||
              </v-list-item-action>
 | 
			
		||||
            </v-list-item>
 | 
			
		||||
          </template>
 | 
			
		||||
        </v-virtual-scroll>
 | 
			
		||||
      </template>
 | 
			
		||||
    </StatCard>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import TheUploadBtn from "@/components/UI/Buttons/TheUploadBtn";
 | 
			
		||||
import ImportSummaryDialog from "@/components/ImportSummaryDialog";
 | 
			
		||||
import { api } from "@/api";
 | 
			
		||||
import StatCard from "./StatCard";
 | 
			
		||||
import ImportDialog from "../Backup/ImportDialog";
 | 
			
		||||
export default {
 | 
			
		||||
  components: { StatCard, ImportDialog, TheUploadBtn, ImportSummaryDialog },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      color: "secondary",
 | 
			
		||||
      selectedName: "",
 | 
			
		||||
      selectedDate: "",
 | 
			
		||||
      loading: false,
 | 
			
		||||
      events: [],
 | 
			
		||||
      availableBackups: [],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    total() {
 | 
			
		||||
      return this.availableBackups.length;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.getAvailableBackups();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async getAvailableBackups() {
 | 
			
		||||
      const response = await api.backups.requestAvailable();
 | 
			
		||||
      this.availableBackups = response.imports;
 | 
			
		||||
      console.log(this.availableBackups);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async deleteBackup(name) {
 | 
			
		||||
      this.loading = true;
 | 
			
		||||
      await api.backups.delete(name);
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
      this.getAvailableBackups();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    openDialog(backup) {
 | 
			
		||||
      this.selectedDate = backup.date;
 | 
			
		||||
      this.selectedName = backup.name;
 | 
			
		||||
      this.$refs.import_dialog.open();
 | 
			
		||||
    },
 | 
			
		||||
    async importBackup(data) {
 | 
			
		||||
      this.loading = true;
 | 
			
		||||
      const response = await api.backups.import(data.name, data);
 | 
			
		||||
      if (response) {
 | 
			
		||||
        const importData = response.data;
 | 
			
		||||
        this.$refs.report.open(importData);
 | 
			
		||||
      }
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async createBackup() {
 | 
			
		||||
      this.loading = true;
 | 
			
		||||
 | 
			
		||||
      let data = {
 | 
			
		||||
        tag: this.tag,
 | 
			
		||||
        options: {
 | 
			
		||||
          recipes: true,
 | 
			
		||||
          settings: true,
 | 
			
		||||
          themes: true,
 | 
			
		||||
          users: true,
 | 
			
		||||
          groups: true,
 | 
			
		||||
        },
 | 
			
		||||
        templates: [],
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      if (await api.backups.create(data)) {
 | 
			
		||||
        this.getAvailableBackups();
 | 
			
		||||
      }
 | 
			
		||||
      this.loading = false;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										110
									
								
								frontend/src/pages/Admin/Dashboard/EventViewer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								frontend/src/pages/Admin/Dashboard/EventViewer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <StatCard icon="mdi-bell-ring" :color="color">
 | 
			
		||||
      <template v-slot:after-heading>
 | 
			
		||||
        <div class="ml-auto text-right">
 | 
			
		||||
          <div class="body-3 grey--text font-weight-light" v-text="'Events'" />
 | 
			
		||||
 | 
			
		||||
          <h3 class="display-2 font-weight-light text--primary">
 | 
			
		||||
            <small> {{ total }} </small>
 | 
			
		||||
          </h3>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
      <div class="d-flex row py-3 justify-end">
 | 
			
		||||
        <v-btn class="mx-2" small :color="color" @click="deleteAll">
 | 
			
		||||
          <v-icon left> mdi-notification-clear-all </v-icon> Clear
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
      <template v-slot:bottom>
 | 
			
		||||
        <v-virtual-scroll height="290" item-height="70" :items="events">
 | 
			
		||||
          <template v-slot:default="{ item }">
 | 
			
		||||
            <v-list-item>
 | 
			
		||||
              <v-list-item-avatar>
 | 
			
		||||
                <v-icon large dark :color="icons[item.category].color">
 | 
			
		||||
                  {{ icons[item.category].icon }}
 | 
			
		||||
                </v-icon>
 | 
			
		||||
              </v-list-item-avatar>
 | 
			
		||||
 | 
			
		||||
              <v-list-item-content>
 | 
			
		||||
                <v-list-item-title v-text="item.title"></v-list-item-title>
 | 
			
		||||
 | 
			
		||||
                <v-list-item-subtitle v-text="item.text"></v-list-item-subtitle>
 | 
			
		||||
                <v-list-item-subtitle>
 | 
			
		||||
                  {{ $d(Date.parse(item.timeStamp), "long") }}
 | 
			
		||||
                </v-list-item-subtitle>
 | 
			
		||||
              </v-list-item-content>
 | 
			
		||||
 | 
			
		||||
              <v-list-item-action class="ml-auto">
 | 
			
		||||
                <v-btn large icon @click="deleteEvent(item.id)">
 | 
			
		||||
                  <v-icon color="error">mdi-delete</v-icon>
 | 
			
		||||
                </v-btn>
 | 
			
		||||
              </v-list-item-action>
 | 
			
		||||
            </v-list-item>
 | 
			
		||||
          </template>
 | 
			
		||||
        </v-virtual-scroll>
 | 
			
		||||
      </template>
 | 
			
		||||
    </StatCard>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { api } from "@/api";
 | 
			
		||||
import StatCard from "./StatCard";
 | 
			
		||||
export default {
 | 
			
		||||
  components: { StatCard },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      color: "secondary",
 | 
			
		||||
      total: 0,
 | 
			
		||||
      events: [],
 | 
			
		||||
      icons: {
 | 
			
		||||
        general: {
 | 
			
		||||
          icon: "mdi-information",
 | 
			
		||||
          color: "info",
 | 
			
		||||
        },
 | 
			
		||||
        recipe: {
 | 
			
		||||
          icon: "mdi-silverware-fork-knife",
 | 
			
		||||
          color: "primary",
 | 
			
		||||
        },
 | 
			
		||||
        backup: {
 | 
			
		||||
          icon: "mdi-backup-restore",
 | 
			
		||||
          color: "primary",
 | 
			
		||||
        },
 | 
			
		||||
        schedule: {
 | 
			
		||||
          icon: "mdi-calendar-clock",
 | 
			
		||||
          color: "primary",
 | 
			
		||||
        },
 | 
			
		||||
        migration: {
 | 
			
		||||
          icon: "mdi-database-import",
 | 
			
		||||
          color: "primary",
 | 
			
		||||
        },
 | 
			
		||||
        signup: {
 | 
			
		||||
          icon: "mdi-account",
 | 
			
		||||
          color: "primary",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.getEvents();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async getEvents() {
 | 
			
		||||
      const events = await api.about.getEvents();
 | 
			
		||||
      this.events = events.events;
 | 
			
		||||
      this.total = events.total;
 | 
			
		||||
    },
 | 
			
		||||
    async deleteEvent(id) {
 | 
			
		||||
      await api.about.deleteEvent(id);
 | 
			
		||||
      this.getEvents();
 | 
			
		||||
    },
 | 
			
		||||
    async deleteAll() {
 | 
			
		||||
      await api.about.deleteAllEvents();
 | 
			
		||||
      this.getEvents();
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										100
									
								
								frontend/src/pages/Admin/Dashboard/StatCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								frontend/src/pages/Admin/Dashboard/StatCard.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
w<template>
 | 
			
		||||
  <v-card v-bind="$attrs" :class="classes" class="v-card--material pa-3">
 | 
			
		||||
    <div class="d-flex grow flex-wrap">
 | 
			
		||||
      <v-sheet
 | 
			
		||||
        :color="color"
 | 
			
		||||
        :max-height="icon ? 90 : undefined"
 | 
			
		||||
        :width="icon ? 'auto' : '100%'"
 | 
			
		||||
        elevation="6"
 | 
			
		||||
        class="text-start v-card--material__heading mb-n6 mt-n10 pa-7"
 | 
			
		||||
        dark
 | 
			
		||||
      >
 | 
			
		||||
        <v-icon v-if="icon" size="40" v-text="icon" />
 | 
			
		||||
        <div v-if="text" class="headline font-weight-thin" v-text="text" />
 | 
			
		||||
      </v-sheet>
 | 
			
		||||
 | 
			
		||||
      <div v-if="$slots['after-heading']" class="ml-auto">
 | 
			
		||||
        <slot name="after-heading" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <slot />
 | 
			
		||||
 | 
			
		||||
    <template v-if="$slots.actions">
 | 
			
		||||
      <v-divider class="mt-2" />
 | 
			
		||||
 | 
			
		||||
      <v-card-actions class="pb-0">
 | 
			
		||||
        <slot name="actions" />
 | 
			
		||||
      </v-card-actions>
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <template v-if="$slots.bottom">
 | 
			
		||||
      <v-divider class="mt-2" />
 | 
			
		||||
 | 
			
		||||
      <div class="pb-0">
 | 
			
		||||
        <slot name="bottom" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </v-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
  name: "MaterialCard",
 | 
			
		||||
 | 
			
		||||
  props: {
 | 
			
		||||
    avatar: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: "",
 | 
			
		||||
    },
 | 
			
		||||
    color: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: "primary",
 | 
			
		||||
    },
 | 
			
		||||
    icon: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: undefined,
 | 
			
		||||
    },
 | 
			
		||||
    image: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
    text: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: "",
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
      type: String,
 | 
			
		||||
      default: "",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  computed: {
 | 
			
		||||
    classes() {
 | 
			
		||||
      return {
 | 
			
		||||
        "v-card--material--has-heading": this.hasHeading,
 | 
			
		||||
      };
 | 
			
		||||
    },
 | 
			
		||||
    hasHeading() {
 | 
			
		||||
      return false;
 | 
			
		||||
    },
 | 
			
		||||
    hasAltHeading() {
 | 
			
		||||
      return false;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="sass">
 | 
			
		||||
.v-card--material
 | 
			
		||||
  &__avatar
 | 
			
		||||
    position: relative
 | 
			
		||||
    top: -64px
 | 
			
		||||
    margin-bottom: -32px
 | 
			
		||||
 | 
			
		||||
    &__heading
 | 
			
		||||
      position: relative
 | 
			
		||||
      top: -40px
 | 
			
		||||
      transition: .3s ease
 | 
			
		||||
      z-index: 1
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										119
									
								
								frontend/src/pages/Admin/Dashboard/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								frontend/src/pages/Admin/Dashboard/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="mt-10">
 | 
			
		||||
    <v-row>
 | 
			
		||||
      <v-col cols="12" sm="12" md="4">
 | 
			
		||||
        <StatCard icon="mdi-silverware-fork-knife">
 | 
			
		||||
          <template v-slot:after-heading>
 | 
			
		||||
            <div class="ml-auto text-right">
 | 
			
		||||
              <div class="body-3 grey--text font-weight-light" v-text="'Recipes'" />
 | 
			
		||||
 | 
			
		||||
              <h3 class="display-2 font-weight-light text--primary">
 | 
			
		||||
                <small> {{ statistics.totalRecipes }}</small>
 | 
			
		||||
              </h3>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-slot:actions>
 | 
			
		||||
            <div class="d-flex row py-3 justify-space-around">
 | 
			
		||||
              <v-btn small color="primary" :to="{ path: '/admin/toolbox/', query: { tab: 'organize', filter: 'tag' } }">
 | 
			
		||||
                <v-icon left> mdi-tag </v-icon> Untagged {{ statistics.untaggedRecipes }}
 | 
			
		||||
              </v-btn>
 | 
			
		||||
              <v-btn
 | 
			
		||||
                small
 | 
			
		||||
                color="primary"
 | 
			
		||||
                :to="{ path: '/admin/toolbox/', query: { tab: 'organize', filter: 'category' } }"
 | 
			
		||||
              >
 | 
			
		||||
                <v-icon left> mdi-tag </v-icon> Uncategorized {{ statistics.uncategorizedRecipes }}
 | 
			
		||||
              </v-btn>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </StatCard>
 | 
			
		||||
      </v-col>
 | 
			
		||||
      <v-col cols="12" sm="12" md="4">
 | 
			
		||||
        <StatCard icon="mdi-account">
 | 
			
		||||
          <template v-slot:after-heading>
 | 
			
		||||
            <div class="ml-auto text-right">
 | 
			
		||||
              <div class="body-3 grey--text font-weight-light" v-text="'Users'" />
 | 
			
		||||
 | 
			
		||||
              <h3 class="display-2 font-weight-light text--primary">
 | 
			
		||||
                <small> {{ statistics.totalUsers }}</small>
 | 
			
		||||
              </h3>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-slot:actions>
 | 
			
		||||
            <div class="ml-auto">
 | 
			
		||||
              <v-btn color="primary" small to="/admin/manage-users?tab=users">
 | 
			
		||||
                <v-icon left>mdi-account</v-icon>
 | 
			
		||||
                Manage Users
 | 
			
		||||
              </v-btn>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </StatCard>
 | 
			
		||||
      </v-col>
 | 
			
		||||
      <v-col cols="12" sm="12" md="4">
 | 
			
		||||
        <StatCard icon="mdi-account-group">
 | 
			
		||||
          <template v-slot:after-heading>
 | 
			
		||||
            <div class="ml-auto text-right">
 | 
			
		||||
              <div class="body-3 grey--text font-weight-light" v-text="'Groups'" />
 | 
			
		||||
 | 
			
		||||
              <h3 class="display-2 font-weight-light text--primary">
 | 
			
		||||
                <small> {{ statistics.totalGroups }}</small>
 | 
			
		||||
              </h3>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template v-slot:actions>
 | 
			
		||||
            <div class="ml-auto">
 | 
			
		||||
              <v-btn color="primary" small to="/admin/manage-users?tab=groups">
 | 
			
		||||
                <v-icon left>mdi-account-group</v-icon>
 | 
			
		||||
                Manage Groups
 | 
			
		||||
              </v-btn>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </StatCard>
 | 
			
		||||
      </v-col>
 | 
			
		||||
    </v-row>
 | 
			
		||||
    <v-row class="mt-10">
 | 
			
		||||
      <v-col cols="12" sm="12" lg="6">
 | 
			
		||||
        <EventViewer />
 | 
			
		||||
      </v-col>
 | 
			
		||||
      <v-col cols="12" sm="12" lg="6"> <BackupViewer /> </v-col>
 | 
			
		||||
    </v-row>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { api } from "@/api";
 | 
			
		||||
import StatCard from "./StatCard";
 | 
			
		||||
import EventViewer from "./EventViewer";
 | 
			
		||||
import BackupViewer from "./BackupViewer";
 | 
			
		||||
export default {
 | 
			
		||||
  components: { StatCard, EventViewer, BackupViewer },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      statistics: {
 | 
			
		||||
        totalGroups: 0,
 | 
			
		||||
        totalRecipes: 0,
 | 
			
		||||
        totalUsers: 0,
 | 
			
		||||
        uncategorizedRecipes: 0,
 | 
			
		||||
        untaggedRecipes: 0,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.getStatistics();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async getStatistics() {
 | 
			
		||||
      this.statistics = await api.meta.getStatistics();
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.grid-style {
 | 
			
		||||
  flex-grow: inherit;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -160,10 +160,10 @@ export default {
 | 
			
		||||
  methods: {
 | 
			
		||||
    updateClipboard(newClip) {
 | 
			
		||||
      navigator.clipboard.writeText(newClip).then(
 | 
			
		||||
        function() {
 | 
			
		||||
        () => {
 | 
			
		||||
          console.log("Copied", newClip);
 | 
			
		||||
        },
 | 
			
		||||
        function() {
 | 
			
		||||
        () => {
 | 
			
		||||
          console.log("Copy Failed", newClip);
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
@@ -4,30 +4,30 @@
 | 
			
		||||
      <v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
 | 
			
		||||
        <v-tabs-slider></v-tabs-slider>
 | 
			
		||||
 | 
			
		||||
        <v-tab>
 | 
			
		||||
        <v-tab href="#users">
 | 
			
		||||
          {{ $t("user.users") }}
 | 
			
		||||
          <v-icon>mdi-account</v-icon>
 | 
			
		||||
        </v-tab>
 | 
			
		||||
 | 
			
		||||
        <v-tab>
 | 
			
		||||
        <v-tab href="#sign-ups">
 | 
			
		||||
          {{ $t("signup.sign-up-links") }}
 | 
			
		||||
          <v-icon>mdi-account-plus-outline</v-icon>
 | 
			
		||||
        </v-tab>
 | 
			
		||||
 | 
			
		||||
        <v-tab>
 | 
			
		||||
        <v-tab href="#groups">
 | 
			
		||||
          {{ $t("group.groups") }}
 | 
			
		||||
          <v-icon>mdi-account-group</v-icon>
 | 
			
		||||
        </v-tab>
 | 
			
		||||
      </v-tabs>
 | 
			
		||||
 | 
			
		||||
      <v-tabs-items v-model="tab">
 | 
			
		||||
        <v-tab-item>
 | 
			
		||||
        <v-tab-item value="users">
 | 
			
		||||
          <TheUserTable />
 | 
			
		||||
        </v-tab-item>
 | 
			
		||||
        <v-tab-item>
 | 
			
		||||
        <v-tab-item value="sign-ups">
 | 
			
		||||
          <TheSignUpTable />
 | 
			
		||||
        </v-tab-item>
 | 
			
		||||
        <v-tab-item>
 | 
			
		||||
        <v-tab-item value="groups">
 | 
			
		||||
          <GroupDashboard />
 | 
			
		||||
        </v-tab-item>
 | 
			
		||||
      </v-tabs-items>
 | 
			
		||||
@@ -42,9 +42,17 @@ import TheSignUpTable from "./TheSignUpTable";
 | 
			
		||||
export default {
 | 
			
		||||
  components: { TheUserTable, GroupDashboard, TheSignUpTable },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      tab: 0,
 | 
			
		||||
    };
 | 
			
		||||
    return {};
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    tab: {
 | 
			
		||||
      set(tab) {
 | 
			
		||||
        this.$router.replace({ query: { ...this.$route.query, tab } });
 | 
			
		||||
      },
 | 
			
		||||
      get() {
 | 
			
		||||
        return this.$route.query.tab;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.$store.dispatch("requestAllGroups");
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								frontend/src/pages/Admin/ToolBox/RecipeOrganizer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								frontend/src/pages/Admin/ToolBox/RecipeOrganizer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-card outlined class="mt-n1">
 | 
			
		||||
    <div class="d-flex justify-center align-center pa-2 flex-wrap">
 | 
			
		||||
      <v-btn-toggle v-model="filter" mandatory color="primary">
 | 
			
		||||
        <v-btn small value="category">
 | 
			
		||||
          <v-icon>mdi-tag-multiple</v-icon>
 | 
			
		||||
          {{ $t("category.category") }}
 | 
			
		||||
        </v-btn>
 | 
			
		||||
 | 
			
		||||
        <v-btn small value="tag">
 | 
			
		||||
          <v-icon>mdi-tag-multiple</v-icon>
 | 
			
		||||
          {{ $t("tag.tags") }}
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </v-btn-toggle>
 | 
			
		||||
      <v-spacer v-if="!isMobile"> </v-spacer>
 | 
			
		||||
 | 
			
		||||
      <FuseSearchBar :raw-data="allItems" @results="filterItems" :search="searchString">
 | 
			
		||||
        <v-text-field
 | 
			
		||||
          v-model="searchString"
 | 
			
		||||
          clearable
 | 
			
		||||
          solo
 | 
			
		||||
          dense
 | 
			
		||||
          class="mx-2"
 | 
			
		||||
          hide-details
 | 
			
		||||
          single-line
 | 
			
		||||
          :placeholder="$t('search.search')"
 | 
			
		||||
          prepend-inner-icon="mdi-magnify"
 | 
			
		||||
        >
 | 
			
		||||
        </v-text-field>
 | 
			
		||||
      </FuseSearchBar>
 | 
			
		||||
    </div>
 | 
			
		||||
  </v-card>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import FuseSearchBar from "@/components/UI/Search/FuseSearchBar";
 | 
			
		||||
export default {
 | 
			
		||||
  components: { FuseSearchBar },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      buttonToggle: 0,
 | 
			
		||||
      allItems: [],
 | 
			
		||||
      searchString: "",
 | 
			
		||||
      searchResults: [],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    isMobile() {
 | 
			
		||||
      return this.$vuetify.breakpoint.name === "xs";
 | 
			
		||||
    },
 | 
			
		||||
    isCategory() {
 | 
			
		||||
      return this.buttonToggle === 0;
 | 
			
		||||
    },
 | 
			
		||||
    filter: {
 | 
			
		||||
      set(filter) {
 | 
			
		||||
        this.$router.replace({ query: { ...this.$route.query, filter } });
 | 
			
		||||
      },
 | 
			
		||||
      get() {
 | 
			
		||||
        return this.$route.query.filter;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    filterItems(val) {
 | 
			
		||||
      this.searchResults = val.map(x => x.item);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
</style>
 | 
			
		||||
@@ -4,20 +4,25 @@
 | 
			
		||||
      <v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
 | 
			
		||||
        <v-tabs-slider></v-tabs-slider>
 | 
			
		||||
 | 
			
		||||
        <v-tab>
 | 
			
		||||
        <v-tab href="#category-editor">
 | 
			
		||||
          {{ $t("recipe.categories") }}
 | 
			
		||||
          <v-icon>mdi-tag-multiple-outline</v-icon>
 | 
			
		||||
        </v-tab>
 | 
			
		||||
 | 
			
		||||
        <v-tab>
 | 
			
		||||
        <v-tab href="#tag-editor">
 | 
			
		||||
          {{ $t("tag.tags") }}
 | 
			
		||||
          <v-icon>mdi-tag-multiple-outline</v-icon>
 | 
			
		||||
        </v-tab>
 | 
			
		||||
        <v-tab href="#organize">
 | 
			
		||||
          Organize
 | 
			
		||||
          <v-icon>mdi-broom</v-icon>
 | 
			
		||||
        </v-tab>
 | 
			
		||||
      </v-tabs>
 | 
			
		||||
 | 
			
		||||
      <v-tabs-items v-model="tab">
 | 
			
		||||
        <v-tab-item><CategoryTagEditor :is-tags="false"/></v-tab-item>
 | 
			
		||||
        <v-tab-item><CategoryTagEditor :is-tags="true" /> </v-tab-item>
 | 
			
		||||
        <v-tab-item value="category-editor"> <CategoryTagEditor :is-tags="false"/></v-tab-item>
 | 
			
		||||
        <v-tab-item value="tag-editor"> <CategoryTagEditor :is-tags="true" /> </v-tab-item>
 | 
			
		||||
        <v-tab-item value="organize"> <RecipeOrganizer :is-tags="true" /> </v-tab-item>
 | 
			
		||||
      </v-tabs-items>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -25,14 +30,24 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import CategoryTagEditor from "./CategoryTagEditor";
 | 
			
		||||
import RecipeOrganizer from "./RecipeOrganizer";
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    CategoryTagEditor,
 | 
			
		||||
    RecipeOrganizer,
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    tab: {
 | 
			
		||||
      set(tab) {
 | 
			
		||||
        this.$router.replace({ query: { ...this.$route.query, tab } });
 | 
			
		||||
      },
 | 
			
		||||
      get() {
 | 
			
		||||
        return this.$route.query.tab;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      tab: 0,
 | 
			
		||||
    };
 | 
			
		||||
    return {};
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user