Skip to content

CMS Structure (Monorepo)

We generate the CMS according to the following structure

txt
.
├── apps
│   ├── api
│   ├── cms
│   │   └── src
│   │       ├── api
│   │       │   └── category.ts
│   │       ├── router
│   │       │   ├── index.ts
│   │       │   └── modules
│   │       │       └── category.ts
│   │       ├── uses
│   │       │   └── category
│   │       │       ├── form.tsx
│   │       │       └── table.tsx
│   │       └── views
│   │           └── category
│   │               ├── Form.vue
│   │               └── index.vue
│   └── frontend
├── packages
│   ├── ...
│   └── common
│       ├── src
│       │   └── vue-i18n-locales.generated.json
│       └── models
│           └── category.ts
└── ...

API Integration

ts
class CategoryResource extends Resource<Category> {
  constructor() {
    super("/v1/categories");
  }
}

export function useCategoryApis() {
  const categoryResource = new CategoryResource();

  const getCategories = (query: IQuery, props?: AxiosRequestConfig) =>
    categoryResource.getAll(query, props);
  const getCategory = (id: number | string, props?: AxiosRequestConfig) =>
    categoryResource.getOne(id, props);
  const createCategory = (data: Category, props?: AxiosRequestConfig) =>
    categoryResource.create(data, props);
  const updateCategory = (
    id: number | string,
    data: Partial<Category>,
    props?: AxiosRequestConfig
  ) => categoryResource.update(id, data, props);
  const deleteCategory = (id: number | string, props?: AxiosRequestConfig) =>
    categoryResource.delete(id, props);

  return {
    getCategories,
    getCategory,
    createCategory,
    updateCategory,
    deleteCategory,
  };
}

Router Setup

There are two types of routes here , constantRoutes and asyncRoutes.

1. constantRouterMap

Represents routes that do not require dynamic access control and do not require authentication.

ts
export const constantRouterMap: RouterMapping[] = [
  // router
];

2. asyncRouterMap

Represents pages that require authentication and are dynamically loaded based on the permissions of the current user.

ts
export const asyncRouterMap = [
  ...,
  category, // loaded based on the permissions of the current user
  {
    path: "/:pathMatch(.*)*",
    redirect: "/404",
    hidden: true,
  },
];

Category Route Definition (category.ts)

ts
const category: RouterMapping = {
  path: '/categories',
  name: 'Category',
  meta: {
    ...
    permissions: [PermissionType.VISIT],
  },
  ...
  children: [
    {
      // List
    },
    {
      // Form Create
    },
    {
      // Form Edit
    },
  ],
};

export default category;

Uses (Hooks)

Category Forms (form.tsx)

tsx
export function useCategoryForms() {
  const route = useRoute();
  const { t } = useI18n();
  const id = route.params.id as string;
  const { createCategory, updateCategory } = useCategoryApis();
  const formRef = ref<FormInstance>();
  const form = reactive<Category>({
    id: 0,
    // columns
  });
  const formElement: LaraFormType<Category> = {
    name: "category",
    ref: formRef,
    form: {
      model: form,
      rules: categoryRules(),
    },
    items: [
      // components
    ],
    actions: {
      create: createCategory,
      update: updateCategory,
    },
  };
  return {
    id,
    form,
    state,
    formElement,
  };
}
export function categoryRules(): FormRules {
  return {
    // validation
  };
}

Category Table (table.tsx)

tsx
export function useCategoryTables() {
  const { getCategories, deleteCategory } = useCategoryApis();
  const { t } = useI18n();
  const table: LaraTableType<Category> = {
    name: "category",
    actions: {
      getAll: getCategories,
      delete: deleteCategory,
    },
    query: {
      orderBy: "-id", // orderby
      include: [], // get relationship
      search: {
        column: "name,content,posts.name", // Where like with columns
      },
      date: {
        column: "categories.updated_at", // Filter between data with column
      },
      filter: "", // Similar https://www.jsonapi.net/usage/reading/filtering.html
      select: "id,name", // Specific select columns
      pagination: {
        page: 1,
        cursor: "string",
        limit: 25,
        type: "default", // 'simple' | 'cursor' | 'default';
      },
      // add any query string
    },
    columns: [
      {
        field: "id",
        type: "string",
        width: 80,
        sortable: "custom",
        align: "center",
        headerAlign: "center",
      },
      //
    ],
  };
  return {
    table,
  };
}

Views

Form.vue

vue
<script setup lang="ts">
const { t } = useI18n();
const { id, form, state, formElement } = useCategoryForms();
const { getCategory } = useCategoryApis();
const coreStore = useCoreStore();
const cancelToken = useCancelToken();

onBeforeMount(async () => {
  coreStore.setLoading(true);
  if (id) {
    const {
      data: { data: category },
    } = await getCategory(id, { cancelToken });
    objectAssign(form, category);
  }
  coreStore.setLoading(false);
});
</script>

<template>
  <el-card>
    <template #header>
      <h3>{{ id ? t("route.category_edit") : t("route.category_create") }}</h3>
    </template>
    <LaraForm :form="formElement" />
  </el-card>
</template>

UI Preview

index.vue

vue
<script setup lang="ts">
const { t } = useI18n();
const { table } = useCategoryTables();
</script>

<template>
  <el-card>
    <template #header>
      <div class="flex items-center justify-between">
        <h3>{{ t("route.category_overview") }}</h3>
        <router-link
          v-slot="{ href, navigate }"
          :to="{ name: 'CategoryCreate' }"
          custom
        >
          <a
            v-permission="[PermissionType.CREATE]"
            :href="href"
            class="pan-btn pan--primary"
            @click="navigate"
          >
            <el-icon class="el-icon--left">
              <IconPlus />
            </el-icon>
            {{ t("button.store") }}
          </a>
        </router-link>
      </div>
    </template>
    <LaraTable :table="table" />
  </el-card>
</template>

UI Preview

Model Interface

ts
export interface Category {
  id: number;
  // columns
}