fix(templates): address review feedback — templates page, proper service, fetch together with archived
This commit is contained in:
parent
00645f50b6
commit
b3d5eb01dc
|
|
@ -49,6 +49,16 @@
|
|||
{{ $t('project.projects') }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink
|
||||
:to="{ name: 'templates.index'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
<Icon icon="copy" />
|
||||
</span>
|
||||
{{ $t('project.template.title') }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink
|
||||
v-shortcut="'KeyG KeyA'"
|
||||
|
|
@ -108,20 +118,6 @@
|
|||
:can-collapse="true"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<nav
|
||||
v-if="templateProjects.length"
|
||||
class="menu"
|
||||
>
|
||||
<span class="menu-label">
|
||||
{{ $t('project.template.title') }}
|
||||
</span>
|
||||
<ProjectsNavigation
|
||||
:model-value="templateProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="true"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<PoweredByLink
|
||||
|
|
@ -160,7 +156,6 @@ const {sidebarWidth, isResizing, startResize, isMobile} = useSidebarResize()
|
|||
const projects = computed(() => projectStore.notArchivedRootProjects as IProject[])
|
||||
const favoriteProjects = computed(() => projectStore.favoriteProjects as IProject[])
|
||||
const savedFilterProjects = computed(() => projectStore.savedFilterProjects as IProject[])
|
||||
const templateProjects = computed(() => projectStore.templateProjects as IProject[])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
{{ $t('menu.duplicate') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-if="!project.isTemplate"
|
||||
icon="copy"
|
||||
@click="saveAsTemplate"
|
||||
>
|
||||
|
|
@ -146,7 +147,7 @@ import {useConfigStore} from '@/stores/config'
|
|||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {PERMISSIONS} from '@/constants/permissions'
|
||||
import AbstractService from '@/services/abstractService'
|
||||
import ProjectTemplateService from '@/services/projectTemplateService'
|
||||
import {success} from '@/message'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
|
|
@ -181,9 +182,7 @@ const isDefaultProject = computed(() => props.project?.id === authStore.settings
|
|||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
async function saveAsTemplate() {
|
||||
const templateService = new AbstractService({
|
||||
create: '/projects/{projectId}/template',
|
||||
})
|
||||
const templateService = new ProjectTemplateService()
|
||||
const response = await templateService.create({projectId: props.project.id})
|
||||
if (response.project) {
|
||||
projectStore.setProject(response.project)
|
||||
|
|
|
|||
|
|
@ -399,8 +399,9 @@
|
|||
"saveAsTemplate": "Save as Template",
|
||||
"saveAsTemplateSuccess": "Project saved as template successfully.",
|
||||
"useTemplate": "Use a template",
|
||||
"selectTemplate": "Select a template...",
|
||||
"createFromTemplate": "Creating project from template..."
|
||||
"selectTemplate": "Select a template\u2026",
|
||||
"createFromTemplate": "Creating project from template\u2026",
|
||||
"none": "You don't have any templates yet. Save a project as a template to get started."
|
||||
},
|
||||
"edit": {
|
||||
"header": "Edit This Project",
|
||||
|
|
|
|||
|
|
@ -241,6 +241,11 @@ const router = createRouter({
|
|||
name: 'projects.index',
|
||||
component: () => import('@/views/project/ListProjects.vue'),
|
||||
},
|
||||
{
|
||||
path: '/templates',
|
||||
name: 'templates.index',
|
||||
component: () => import('@/views/project/ListTemplates.vue'),
|
||||
},
|
||||
{
|
||||
path: '/projects/new',
|
||||
name: 'project.create',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import AbstractService from './abstractService'
|
||||
|
||||
export default class ProjectTemplateService extends AbstractService {
|
||||
constructor() {
|
||||
super({
|
||||
create: '/projects/{projectId}/template',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -264,19 +264,11 @@ export const useProjectStore = defineStore('project', () => {
|
|||
let page = 1
|
||||
try {
|
||||
do {
|
||||
const newProjects = await projectService.getAll({}, {is_archived: true, expand: 'permissions'}, page) as IProject[]
|
||||
const newProjects = await projectService.getAll({}, {is_archived: true, is_template: true, expand: 'permissions'}, page) as IProject[]
|
||||
loadedProjects.push(...newProjects)
|
||||
page++
|
||||
} while (page <= projectService.totalPages)
|
||||
|
||||
// Fetch templates separately
|
||||
let templatePage = 1
|
||||
do {
|
||||
const newTemplates = await projectService.getAll({}, {is_template: true, expand: 'permissions'}, templatePage) as IProject[]
|
||||
loadedProjects.push(...newTemplates)
|
||||
templatePage++
|
||||
} while (templatePage <= projectService.totalPages)
|
||||
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div
|
||||
class="content loader-container"
|
||||
:class="{'is-loading': loading}"
|
||||
>
|
||||
<header class="project-header">
|
||||
<h3>{{ $t('project.template.title') }}</h3>
|
||||
</header>
|
||||
|
||||
<p v-if="!loading && templates.length === 0">
|
||||
{{ $t('project.template.none') }}
|
||||
</p>
|
||||
|
||||
<ProjectCardGrid
|
||||
v-else
|
||||
:projects="templates"
|
||||
:show-archived="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import ProjectCardGrid from '@/components/project/partials/ProjectCardGrid.vue'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
const {t} = useI18n()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
useTitle(() => t('project.template.title'))
|
||||
|
||||
const loading = computed(() => projectStore.isLoading)
|
||||
const templates = computed(() => projectStore.templateProjects as IProject[])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.project-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -593,7 +593,7 @@ func getUserProjectsStatement(userID int64, search string) *builder.Builder {
|
|||
}
|
||||
|
||||
return builder.
|
||||
Select("l.id, l.title, l.description, l.identifier, l.hex_color, l.owner_id, l.parent_project_id, l.is_archived, l.background_file_id, l.background_blur_hash, l.position, l.created, l.updated").
|
||||
Select("l.id, l.title, l.description, l.identifier, l.hex_color, l.owner_id, l.parent_project_id, l.is_archived, l.is_template, l.background_file_id, l.background_blur_hash, l.position, l.created, l.updated").
|
||||
From("projects", "l").
|
||||
Join("LEFT", "team_projects tl", "tl.project_id = l.id").
|
||||
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
|
||||
|
|
@ -658,7 +658,7 @@ func getAllProjectsForUser(s *xorm.Session, userID int64, opts *projectOptions)
|
|||
|
||||
baseQuery := querySQLString + `
|
||||
UNION ALL
|
||||
SELECT p.id, p.title, p.description, p.identifier, p.hex_color, p.owner_id, p.parent_project_id, (ap.is_archived OR p.is_archived) AS is_archived, p.background_file_id, p.background_blur_hash, p.position, p.created, p.updated FROM projects p
|
||||
SELECT p.id, p.title, p.description, p.identifier, p.hex_color, p.owner_id, p.parent_project_id, (ap.is_archived OR p.is_archived) AS is_archived, p.is_template, p.background_file_id, p.background_blur_hash, p.position, p.created, p.updated FROM projects p
|
||||
INNER JOIN all_projects ap ON p.parent_project_id = ap.id`
|
||||
|
||||
columnStr := strings.Join([]string{
|
||||
|
|
|
|||
Loading…
Reference in New Issue