basics

package
v3.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2024 License: MIT Imports: 13 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Activity = Doc(
	Markdown(`
QOR5 provides a built-in activity module for recording model operations that may be important for admin users of CMS. These records are designed to be easily queried and audited, and the activity module supports the following features:

* Detailed change logging functionality for model data modifications.
* Allow certain fields to be ignored when comparing modified data, such as the update time.
* Customization of the diffing process for complex field types, like time.Time.
* Customization of the keys used to identify model data.
* Support both automatic and manual CRUD operation recording.
* Provide flexibility to customize the actions other than default CRUD.
* An page for querying the activity log via QOR5 admin

## Initialize the activity package
To initialize activity package with the default configuration, you need to pass a ~presets.Builder~ instance and a database instance.
`),
	ch.Code(generated.NewActivitySample).Language("go"),
	Markdown(`
By default, the activity package uses QOR5 login package's ~~login.UserKey~~ as the default key to fetch the current user from the context. If you want to use your own key, you can use the ~~CreatorContextKey~~ function.

Same with above, the activity package uses the db instance that passed in during initialization to perform db operations. If you need another db to do the work, you can use ~~DBContextKey~~ method.

## Register the models that require activity tracking
This example demonstrates how to register ~~Product~~ into the activity. The activities on the product model will be automatically recorded when it is created, updated, or deleted.
`),
	ch.Code(generated.ActivityRegisterPresetsModelsSample).Language("go"),
	Markdown(`
By default, the activity package will use the primary key as the key to indentify the current model data. You can use ~~SetKeys~~ and ~~AddKeys~~ methods to customize it.

When diffing the modified data, the activity package will ignore the ~~ID~~, ~~CreatedAt~~, ~~UpdatedAt~~, ~~DeletedAt~~ fields. You can either use ~~AddIgnoredFields~~ to append your own fields to the default ignored fields. Or ~~SetIgnoredFields~~ method to replace the default ignored fields.

For special fields like ~~time.Time~~ or media files handled by QOR5 media_library, activity package already handled them. You can use ~~AddTypeHanders~~ method to handle your own field types.

If you want to skip the automatic recording, you can use ~~SkipCreate~~, ~~SkipUpdate~~ and ~~SkipDelete~~ methods.

The Activity package allows for displaying the activities of a record on its editing page. Simply use the ~~EnableActivityInfoTab~~ method to enable this feature. Once enabled, you can customize the format of each activity's display text using the ~~TabHeading~~ method. Additionally, you can make each activity a link to the corresponding record using the ~~SetLink~~ method.

## Record the activity log manually
If you register a preset model into the activity, the activity package will automatically record the activity log for CRUD operations. However, if you need to manually record the activity log for other operations or if you want to register a non-preset model, you can use the following sample code.`),
	ch.Code(generated.ActivityRecordLogSample).Language("go"),
).Title("Activity Log")
View Source
var BasicInputs = Doc(
	Markdown(`
Vuetify provides many form basic inputs, and also with error messages display on fields.

Here is one example:
`),
	ch.Code(generated.VuetifyBasicInputsSample).Language("go"),
	utils.DemoWithSnippetLocation("Vuetify Basic Inputs", examples_vuetify.VuetifyBasicInputsPath, generated.VuetifyBasicInputsSampleLocation),
).Title("Basic Inputs").
	Slug("vuetify-components/basic-inputs")
View Source
var Brand = Doc(
	Markdown(`
Brand refers to the top area of the left menu bar, we provide two functions ~BrandTitle~ and ~BrandFunc~ to customize it.

## Simple customization
If you want only to change the brand string, you can use ~BrandTitle~ to set the string, the string will be displayed in the brand area with ~<H1>~ tag.
`),

	ch.Code(generated.BrandTitleSample).Language("go"),
	utils.DemoWithSnippetLocation("Brand Title", examples.URLPathByFunc(examples_presets.PresetsBrandTitle)+"/brands", generated.BrandTitleSampleLocation),

	Markdown(`
## Full customization
When you opt-in to full brand customization, you can use ~BrandFunc~ to be responsible for drawing for the entire brand area, such as you can put your own logo image in it.
`),

	ch.Code(generated.BrandFuncSample).Language("go"),
	utils.DemoWithSnippetLocation("Brand Func", examples.URLPathByFunc(examples_presets.PresetsBrandFunc)+"/brands", generated.BrandFuncSampleLocation),

	Markdown(`
## Profile
Profile is below the brand area, where you can put the current user's information or others. We provide ~ProfileFunc~ to customize it.
`),

	ch.Code(generated.ProfileSample).Language("go"),
	utils.DemoWithSnippetLocation("Profile", examples.URLPathByFunc(examples_presets.PresetsProfile)+"/brands", generated.ProfileSampleLocation),
).Title("Brand").
	Slug("basics/brand")
View Source
var ConfirmDialog = Doc(
	Markdown(fmt.Sprintf("`%s`", strings.TrimRight(generated.OpenConfirmDialog, ","))+`
 is a pre-defined event used to show a confirm dialog for user to do confirm before executing the actual action.
`+
		`
### Queries
`+fmt.Sprintf("`%s`  ", strings.TrimRight(generated.ConfirmDialogConfirmEvent, ","))+
		`
required  
Usually the value will be *web.Plaid().EventFunc(the actual action event)....Go()*.  
  
`+fmt.Sprintf("`%s`  ", strings.TrimRight(generated.ConfirmDialogPromptText, ","))+
		`
optional  
To customize the prompt text.  
  
`+fmt.Sprintf("`%s`  ", strings.TrimRight(generated.ConfirmDialogDialogPortalName, ","))+
		`
optional  
To use a custom portal for dialog.  
`),
	Markdown(`
## Example
`),
	ch.Code(generated.ConfirmDialogSample).Language("go"),
	utils.DemoWithSnippetLocation(
		"Confirm Dialog",
		path.Join(examples.URLPathByFunc(examples_presets.PresetsConfirmDialog), "/confirm-dialog"),
		generated.ConfirmDialogSampleLocation,
	),
).Slug("basics/confirm-dialog").Title("Confirm Dialog")
View Source
var EditingCustomizations = Doc(
	Markdown(`
Editing an object will be always in a drawer popup. select which fields can edit for each model
by using the ~.Only~ func of ~EditingBuilder~, There are different ways to configure the type
of component that is used to do the editing.

`),
	utils.Anchor(H2(""), "Configure field for a single model"),
	Markdown(`
Use a customized component is as simple as add the extra asset to the preset instance.
And configure the component func on the field:
`),
	ch.Code(generated.PresetsEditingCustomizationDescriptionSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Editing Customization Description Field", examples.URLPathByFunc(examples_presets.PresetsEditingCustomizationDescription)+"/customers", generated.PresetsEditingCustomizationDescriptionSampleLocation),
	Markdown(`
- Added the redactor javascript and css component pack as an extra asset
- Configure the description field to use the component func that returns the ~richeditor.RichEditor~ component
- Set the field name and value of the component
`),
	utils.Anchor(H2(""), "Configure field type for all models"),
	Markdown(`
Set a global field type to component func like the following:
`),
	ch.Code(generated.PresetsEditingCustomizationFileTypeSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Editing Customization File Type", examples.URLPathByFunc(examples_presets.PresetsEditingCustomizationFileType)+"/products", generated.PresetsEditingCustomizationFileTypeSampleLocation),
	Markdown(`
- We define ~MyFile~ to actually be a string
- We set ~FieldDefaults~ for writing, which is the editing drawer popup to be a customized component
- The component show an img tag with the string as src if it's not empty
- The component add a file input for user to upload new file
- The ~SetterFunc~ is called before save the object, it uploads the file to transfer.sh, and get the url back,
  then set the value to ~MainImage~ field

With ~FieldDefaults~ we can write libraries that add customized type for different models to reuse. It can take care
of how to display the edit controls, and How to save the object.

`),
	utils.Anchor(H2(""), "Tabs"),
	Markdown(`
Tabs can be added by using ~AppendTabsPanelFunc~ func on ~EditingBuilder~:
`),
	ch.Code(generated.PresetsEditingCustomizationTabsSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Editing Customization Tabs", examples.URLPathByFunc(examples_presets.PresetsEditingCustomizationTabs)+"/companies", generated.PresetsEditingCustomizationTabsSampleLocation),
	utils.Anchor(H2(""), "Validation"),
	Markdown(`
Field level validation and display on field can be added by implement ~ValidateFunc~,
and set the ~web.ValidationErrors~ result:
`),
	ch.Code(generated.PresetsEditingCustomizationValidationSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Editing Customization Validation", examples.URLPathByFunc(examples_presets.PresetsEditingCustomizationValidation)+"/customers", generated.PresetsEditingCustomizationValidationSampleLocation),
	Markdown(`
- We validate the ~Name~ of the customer must be longer than 10
- If the error happens, If will show below the field

`),
).Title("Editing").
	Slug("presets-guide/editing-customizations")
View Source
var Filter = Doc(
	Markdown(`

Assume we have a ~status~ filed in Post. It has 2 possible values, "draft" and "online". If we want to filter posts by its status. We can add a filter like this:

`),
	ch.Code(generated.FilterSample).Language("go"),
	utils.DemoWithSnippetLocation("Basic filter", examples.URLPathByFunc(examples_presets.PresetsBasicFilter)+"/posts", generated.FilterSampleLocation),

	Markdown(`
### QOR5 now supports 7 types of filter option.

PLEASE NOTE THAT all below sample are required you to provide the ~SQLCondition~ you want to perform.

## 1. Filter by String
Set the ~ItemType~ as ~vuetifyx.ItemTypeString~. No ~Options~ needed.
Under this mode, the filter would work in 2 ways,
1. the target value equal to the input string
2. the target value contains the input string

## 2. Filter by Number
Set the ~ItemType~ as ~vuetifyx.ItemTypeNumber~. No ~Options~ needed.
Under this mode, the filter would work in 4 ways
1. the target value equal to the input number
2. the target value is between the input numbers
3. the target value is greater than the input number
4. the target value is less than the input number
`),

	Markdown(`
## 3. Filter by Date
Set the ~ItemType~ as ~vuetifyx.ItemTypeDate~. No ~Options~ needed.
Under this mode, the filter would render a date picker for users to select.
`),

	Markdown(`
## 4. Filter by Date Range
Set the ~ItemType~ as ~vuetifyx.ItemTypeDateRange~. No ~Options~ needed.
Under this mode, the filter would render 2 date pickers, "from" and "to" for users to select.
`),

	Markdown(`
## 5. Filter by Datetime Range
Set the ~ItemType~ as ~vuetifyx.ItemTypeDatetimeRange~. No ~Options~ needed.
Under this mode, the filter would render 2 *date time* pickers, "from" and "to" for users to select.
`),

	Markdown(`
## 6. Filter by Selectable Items
Set the ~ItemType~ as ~vuetifyx.ItemTypeSelect~. You need to provide ~Options~ like this. The ~Text~ is the text users can see in the selector, the ~Value~ is the value of the selector.
`),

	ch.Code(`Options: []*vuetifyx.SelectItem{
		{Text: "Active", Value: "active"},
		{Text: "Inactive", Value: "inactive"},
	},`),

	Markdown(`
## 7. Filter by Multiple Select
Set the ~ItemType~ as ~vuetifyx.ItemTypeMultipleSelect~. You need to provide ~Options~ like above "Selectable Items". But in this mode, the filter would render the options as multi-selectable checkboxes and the query of this filter becomes ~IN~ and ~NOT IN~.
`),
).Title("Filters").
	Slug("basics/filter")
View Source
var FormHandling = Doc(
	Markdown(`
Form handling is an important part of web development. to make handling form easy,
we have a global form that always be submitted with any event func. What you need to do
is just to give an input a name.

For example:
`),
	ch.Code(generated.FormHandlingSample).Language("go"),
	utils.DemoWithSnippetLocation("Form Handling", examples_web.FormHandlingPagePath, generated.FormHandlingSampleLocation),
	Markdown(`
Use ~.Attr(web.VField("Abc")...)~ to set the field name, make the name matches your data struct field name.
So that you can ~ctx.UnmarshalForm(&fv)~ to set the values to data object. value of input must be set manually to set the initial value of form field.

The fields which are bind with ~.Attr(web.VField("Abc")...)~ are always submitted with every event func. A browser refresh, new page load will clear the form value.

~web.Scope(...).VSlot("{ plaidForm }")~ to nest a new form inside outside form, EventFunc inside will only post form values inside the scope.
`),
).Title("Form Handling").
	Slug("basics/form-handling")
View Source
var HomePage = Doc(
	Markdown(`
    TODO
`),
).Title("Home Page").
	Slug("basics/home_page")
View Source
var I18n = docgo.Doc(
	docgo.Markdown(`
The [i18n package](https://github.com/qor5/x/tree/master/i18n) provides support for internationalization (i18n) in Go applications. 
With the package, you can support multiple languages, 
register messages for each module in each language, and serve multilingual content 
based on the user's preferences.
`),
	h.Br(),
	utils.Demo(
		"I18n",
		path.Join(examples.URLPathByFunc(examples_admin.InternationalizationExample), "/home"),
		"examples/examples_admin/internationalization.go",
	),
	docgo.Markdown(`
## Getting Started
To use the i18n package, you first need to import it into your Go application:
`),
	ch.Code(`import "github.com/qor5/x/v3/i18n"`).Language("go"),
	docgo.Markdown(`
Next, create a new ~Builder~ instance using the ~New()~ function. 
If you want to use it with QOR5, use the ~GetI18n()~ on ~presets.Builder~:
`),
	ch.Code(generated.I18nNew).Language("go"),
	docgo.Markdown(`
The ~Builder~ struct is the central point of the i18n package. 
It holds the supported languages, the messages for each module in each language, 
and the configuration for retrieving the language preference.
`),
	docgo.Markdown(`
## Adding Support Languages
To support multiple languages in your web application, you need to define the languages that you support. 
You can do this by calling the ~SupportLanguages~ function on the ~Builder~ struct:
`),
	ch.Code(generated.I18nSupportLanguages).Language("go"),
	docgo.Markdown(`
The i18n package uses English as the default language. You can add other languages by the ~SupportLanguages~ function.
`),
	docgo.Markdown(`
## Registering Module Messages
Once you have defined the languages, you need to register messages for each module. 
You can do this by the ~RegisterForModule~ function on the ~Builder~ struct:
`),
	ch.Code(generated.I18nRegisterForModule).Language("go"),
	docgo.Markdown(`
The ~RegisterForModule~ function takes three arguments: the language tag, the module key, 
and a pointer to a struct that implements the Messages interface. 
The Messages interface is an empty interface that you can use to define your own messages.

Such a struct might look like this:
`),
	ch.Code(generated.I18nMessagesExample).Language("go"),
	docgo.Markdown(`
If you want to define messages inside the system, 
you can add new variables to the message structure associated with ~presets.ModelsI18nModuleKey~, 
and the variable name definitions follow the camel case.

Such a struct might look like this:
`),
	ch.Code(generated.I18nPresetsMessagesExample).Language("go"),
	docgo.Markdown(`
The ~GetSupportLanguagesFromRequestFunc~ is a method of the ~Builder~ struct in the i18n package.
It allows you to set a function that retrieves the list of supported languages
from an HTTP request, which can be useful in scenarios where the list of supported
languages varies based on the request context.

If you create a separate page, you need to use the ~EnsureLanguage~ to get i18n to work on this page.

The ~EnsureLanguage~ function is an HTTP middleware that ensures the request's language
is properly set and stored. It does this by first checking the query parameters for
a language value, and if found, setting a cookie with that value. If no language
value is present in the query parameters, it looks for the language value in the cookie.

The middleware then determines the best-matching language from the supported languages
based on the "Accept-Language" header of the request. If no match is found,
it defaults to the first supported language. It then sets the language context for
the request, which can be retrieved later by calling the ~MustGetModuleMessages~ function.
`),
	docgo.Markdown(`
## Retrieving Messages
To retrieve module messages in your HTTP handler, you can use the ~MustGetModuleMessages~ function:
`),
	ch.Code(generated.I18nMustGetModuleMessages).Language("go"),
	docgo.Markdown(`
The ~MustGetModuleMessages~ function takes three arguments:
the HTTP request, the module key, and a pointer to a struct
that implements the Messages interface. The function retrieves the messages
for the specified module in the language set by the i18n middleware.
`),
).Slug("basics/i18n").Title("Internationalization")
View Source
var L10n = docgo.Doc(
	docgo.Markdown(`
L10n gives your models the ability to localize for different Locales.  
It can be a catalyst for the adaptation of a product, application, or document content to meet the language, cultural, and other requirements of a specific target market.
    `),
	docgo.Markdown(`
## Define a struct
Define a struct that requires embed ~l10n.Locale~.  
Also this struct must implement ~PrimarySlug() string~ and ~PrimaryColumnValuesBySlug(slug string) map[string]string~.
`),
	ch.Code(generated.L10nModelExample).Language("go"),
	docgo.Markdown(`
## Init a l10n builder
Register locales here.  
You can use ~SupportLocalesFunc~ to determine who can use which locales.
`),
	ch.Code(generated.L10nBuilderExample).Language("go"),
	docgo.Markdown(`
## Configure the model builder
Use ~l10n_view.Configure()~ func to configure l10n view.  
The ~Switch Locale~ ui will appear below the ~Brand~.  
The ~Localize~ ui will appear in the ~RowMenuItem~ under the ~Edit~ and the ~Delete~.  
~Localize~ button is used to copy a piece of data from the current locale to the other locales.
`),
	ch.Code(generated.L10nConfigureExample).Language("go"),
	docgo.Markdown(`
## Full Example
`),
	ch.Code(generated.L10nFullExample).Language("go"),
	utils.DemoWithSnippetLocation(
		"L10n",
		path.Join(examples.URLPathByFunc(examples_admin.LocalizationExample), "/l10n-models"),
		generated.L10nFullExampleLocation,
	),
).Slug("basics/l10n").Title("Localization")
View Source
var Layout = Doc(
	Markdown(`
Presets comes with a built-in layout that works out of the box.  
And there are some ways to customzie the layout/theme.
## Theme
Presets UI is based on [Vuetify](https://v2.vuetifyjs.com/en/), you can modify the Admin theme by configuring the [Vuetify options](https://v2.vuetifyjs.com/en/features/presets/#default-preset)
    `),
	ch.Code(generated.CustomizeVuetifyOptions).Language("go"),
	Markdown(`
## Assets
If you need third-party front-end libraries to achieve some functions, 
you can inject them via the *ExtraAsset* method, and they will be automatically served.
    `),
	ch.Code(generated.InjectAssetViaExtraAsset).Language("go"),
	Markdown(`
you can also call Injector in AssetFunc to add meta, add custom HTML in HEAD and TAIL.
    `),
	ch.Code(generated.InjectAssetViaAssetFunc).Language("go"),
	Markdown(`
## Layout
You can change the entire layout via *LayoutFunc*. The default layout is https://github.com/qor5/admin/blob/1e97c0dd45615fb7593245575ab0fea4f98c58b3/presets/presets.go#L860-L969
### Plain Layout
And We provide [PlainLayout](https://github.com/qor5/admin/blob/1e97c0dd45615fb7593245575ab0fea4f98c58b3/presets/presets.go#L972) which has no UI content except necessary assets. 
It will be helpful when there are some pages completely independent of Presets layout but still need to be consistent with the Presets theme.
    `),
).Slug("basics/layout").Title("Layout")
View Source
var LinkageSelect = Doc(
	Markdown(`
LinkageSelect is a component for multi-level linkage select.
    `),
	ch.Code(generated.VuetifyComponentsLinkageSelect).Language("go"),
	utils.DemoWithSnippetLocation("Vuetify LinkageSelect", examples_vuetifyx.VuetifyComponentsLinkageSelectPath, generated.VuetifyComponentsLinkageSelectLocation),
	Markdown(`
### Filter intergation
    `),
	ch.Code(generated.LinkageSelectFilterItem).Language("go"),
	utils.DemoWithSnippetLocation("LinkageSelect Filter Item", examples.URLPathByFunc(examples_presets.PresetsLinkageSelectFilterItem)+"/addresses", generated.LinkageSelectFilterItemLocation),
).Title("Linkage Select").
	Slug("vuetify-components/linkage-select")
View Source
var Listing = Doc(
	Markdown(`
By the [1 Minute Quick Start](/getting-started/one-minute-quick-start.html), We get a default listing page with default columns, But default columns from database columns rarely fit the needs for any real application. Here we will introduce common customizations on the list page.

- Configure fields that displayed on the page
- Modify the display value
- Display a virtual field
- Default scope
- Extend the dot menu

There would be a runable example at the last.

## Configure fields that displayed on the page
Suppose we added a new model called ~Category~, the ~Post~ belongs to ~Category~. Then we want to display ~CategoryID~ on the list page.
`),

	ch.Code(`
type Post struct {
	ID    uint
	Title string
	Body  string

	CategoryID uint

	UpdatedAt time.Time
	CreatedAt time.Time
}

type Category struct {
	ID   uint
	Name string

	UpdatedAt time.Time
	CreatedAt time.Time
}

postModelBuilder.Listing("ID", "Title", "Body", "CategoryID")
`),

	Markdown(`
## Modify the display value
To display the category name rather than category id in the post listing page. The ~ComponentFunc~ would do the work.
The ~obj~ is the ~Post~ record, and ~field~ is the ~CategoryID~ field of this ~Post~ record. You can get the value by ~field.Value(obj)~ function.
`),

	ch.Code(`postModelBuilder.Listing().Field("CategoryID").Label("Category").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
	c := models.Category{}
	cid, _ := field.Value(obj).(uint)
	if err := db.Where("id = ?", cid).Find(&c).Error; err != nil {
		// ignore err in the example
	}
	return h.Td(h.Text(c.Name))
})
`).Language("go"),

	Markdown(`
## Display virtual fields
`),
	ch.Code(`postModelBuilder.Listing("ID", "Title", "Body", "CategoryID", "VirtualField")
postModelBuilder.Listing().Field("VirtualField").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
	return h.Td(h.Text("virtual field"))
})
`),

	Markdown(`
## DefaultScope
If we want to display ~Post~ with ~disabled=false~ only. Use the ~Listing().SearcherFunc(...)~ to apply SQL conditions.
`),
	ch.Code(`postModelBuilder.Listing().SearcherFunc(func(model interface{}, params *presets.SearchParams, ctx *web.EventContext) (r interface{}, totalCount int, err error){
	qdb := db.Where("disabled != true")
	return gorm2op.DataOperator(qdb).Search(model, params, ctx)
})
`),

	Markdown(`
## Extend the dot menu
You can extend the dot menu by calling the ~RowMenuItem~ function. If you want to overwrite the default ~Edit~ and ~Delete~ link, you can pass the items you wanted to ~Listing().RowMenu()~
`),
	ch.Code(`rmn := postModelBuilder.Listing().RowMenu()
rmn.RowMenuItem("Show").ComponentFunc(func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent {
	return h.Text("Fake Show")
})
`),

	Markdown(`
## Full Example
`),
	ch.Code(generated.PresetsListingSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Listing Customization Fields", examples.URLPathByFunc(examples_admin.ListingExample)+"/posts", generated.PresetsListingSampleLocation),
).Title("Listing").
	Slug("basics/listing")
View Source
var ListingCustomizations = Doc(
	Markdown(`
We get a default listing page with default columns, But default columns from database
columns rarely fit the needs for any real application.

`),
	utils.Anchor(H2(""), "Change List Columns and Component of Field"),
	Markdown(`
Here is how do we change the columns of the list and how to we change the content display of a columns.
`),
	ch.Code(generated.PresetsListingCustomizationFieldsSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Listing Customization Fields", examples.URLPathByFunc(examples_presets.PresetsListingCustomizationFields)+"/customers", generated.PresetsListingCustomizationFieldsSampleLocation),
	Markdown(`
What we did with above code:

- Added a new field to listing table that not exists on the struct ~Customer~
- Define the listing display for the listing table by using the ~Td()~ and fetch the company data from a different table with associated column value
- Link the company name in the listing to link the edit drawer of company
- Limit the edit drawer field to only have ~Name~ and ~CompanyID~
- Made the ~CompanyID~ field a vuetify ~VSelect~ component
- Add companies as a new navigation item, that you can manage companies data
- ~.SearchColumns("name", "email")~ configure the top navigation search box searches which columns with sql like operation
`),

	utils.Anchor(H2(""), "Filters Panel"),
	Markdown(`
Here we continue to add filters for the list
`),
	ch.Code(generated.PresetsListingCustomizationFiltersSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Listing Filters", examples.URLPathByFunc(examples_presets.PresetsListingCustomizationFilters)+"/customers", generated.PresetsListingCustomizationFiltersSampleLocation),
	Markdown(`
~FilterDataFunc~ of ~presets.ListingBuilder~ setup to have the filter menu or not.
And how it will combine the sql conditions when doing query. the filter menu will
change the url query strings with the filter values.

Current we support these types

- ~ItemTypeDate~: set it as a date filter item, which have many switches to support date and date range
- ~ItemTypeNumber~: set it to a number filter item, which have switches to support number and number range
- ~ItemTypeString~: set it to a string filter item, which have contains, and match exactly
- ~ItemTypeSelect~: set it to a select filter item, which have a options of values for selection
`),

	utils.Anchor(H2(""), "Filter Tabs"),
	Markdown(`
Filter tabs is based on Filters configuration. But display as tabs above the list,
You can think it as a short cut that used very frequently to filter something instead of
use the pop up panel of filter.
`),
	ch.Code(generated.PresetsListingCustomizationTabsSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Listing Filter Tabs", examples.URLPathByFunc(examples_presets.PresetsListingCustomizationTabs)+"/customers", generated.PresetsListingCustomizationTabsSampleLocation),
	Markdown(`
~Query~ string name must be from the Filter's item configuration key field.
`),

	utils.Anchor(H2(""), "Bulk Actions"),
	Markdown(`
Bulk actions makes the list row show checkboxes, and you can select one or many rows,
Later do an bulk update data for all of them.

Here is how to use it:
`),
	ch.Code(generated.PresetsListingCustomizationBulkActionsSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Listing Bulk Actions", examples.URLPathByFunc(examples_presets.PresetsListingCustomizationBulkActions)+"/customers", generated.PresetsListingCustomizationBulkActionsSampleLocation),
	Markdown(`
- ~ComponentFunc~ of the bulk action configure the component that will show to user to input after user clicked the bulk action button
- ~UpdateFunc~ configure the logic that the bulk action execute
`),
	utils.Anchor(H2(""), "Search Func"),
	Markdown(`
~SearchFunc~ defines a data processing function for ~ListingBuilder~.
This function searches for a model based on the specified search parameters.
It returns the search results along with the total count of matching records.
You can process the data displayed on the listing page here based on context or custom conditions before pagination.

In the following example, the listing page only displays approved customers.
`),
	ch.Code(generated.PresetsListingCustomizationSearcherSample).Language("go"),
	utils.DemoWithSnippetLocation("Search Func", examples.URLPathByFunc(examples_presets.PresetsListingCustomizationSearcher)+"/customers", generated.PresetsListingCustomizationSearcherSampleLocation),
).Title("Listing Customizations").
	Slug("basics/listing-customizations")
View Source
var Login = Doc(
	Markdown(`
Login package provides comprehensive login authentication logic and related UI interfaces. It is designed to simplify the process of adding user authentication to QOR5-based backend development project.   
In QOR5 admin development, we recommend using [github.com/qor5/admin/login](https://github.com/qor5/admin/tree/main/login), which wraps [github.com/qor5/x/login](https://github.com/qor5/x/tree/master/login) to keep the theme of login UI consistent with Presets and provide more powerful features.
## Basic Usage
The example shows how to enable both username/password login and OAuth login.
    `),
	ch.Code(generated.LoginBasicUsage).Language("go"),
	Markdown(`
## Username/Password Login
To enable Username/Password login, the ~UserModel~ needs to implement the [UserPasser](https://github.com/qor5/x/blob/8f986dddfeaf235fd42bb3361717551d06695517/login/user_pass.go#L13) interface. There is a default implementation - [UserPass](https://github.com/qor5/x/blob/8f986dddfeaf235fd42bb3361717551d06695517/login/user_pass.go#L44).  
    `),
	ch.Code(generated.LoginEnableUserPass).Language("go"),
	Markdown(`
### Change Password
There are three ways to change the password:

1\. Visit the default change password page.

2\. Call the ~OpenChangePasswordDialogEvent~ event to change it in dialog.
    `),
	ch.Code(generated.LoginOpenChangePasswordDialog).Language("go"),
	Markdown(`

3\. Change the password directly in Editing.
    `),
	ch.Code(generated.LoginChangePasswordInEditing).Language("go"),
	Markdown(`
### MaxRetryCount
By default, it allows 5 login attempts with incorrect credentials, and if the limit is exceeded, the user will be locked for 1 hour. This helps to prevent brute-force attacks on the login system. You can call ~MaxRetryCount~ to set the maximum retry count. If you set MaxRetryCount to a value less than or equal to 0, it means there is no limit of login attempts, and the user will not be locked after a certain number of failed login attempts. 
    `),
	ch.Code(generated.LoginSetMaxRetryCount).Language("go"),
	Markdown(`
### TOTP
There is TOTP (Time-based One-time Password) functionality out of the box, which is enabled by default.
    `),
	ch.Code(generated.LoginSetTOTP).Language("go"),
	Markdown(`
### Google reCAPTCHA
Google reCAPTCHA is disabled by default.
    `),
	ch.Code(generated.LoginSetRecaptcha).Language("go"),
	Markdown(`
## OAuth Login
OAuth login is based on [goth](https://github.com/markbates/goth).   
OAuth login does not require a ~UserModel~. If there is a ~UserModel~, it needs to implement the [OAuthUser](https://github.com/qor5/x/blob/8f986dddfeaf235fd42bb3361717551d06695517/login/oauth_user.go#L5) interface. There is a default implementation - [OAuthInfo](https://github.com/qor5/x/blob/8f986dddfeaf235fd42bb3361717551d06695517/login/oauth_user.go#L13).  
    `),
	ch.Code(generated.LoginEnableOAuth).Language("go"),
	Markdown(`
## Session Secure
The [SessionSecurer](https://github.com/qor5/x/blob/8f986dddfeaf235fd42bb3361717551d06695517/login/session_secure.go#L11) provides a way to manage unique salt for a user record. There is a default implementation - [SessionSecure](https://github.com/qor5/x/blob/8f986dddfeaf235fd42bb3361717551d06695517/login/session_secure.go#L16).  
    `),
	ch.Code(generated.LoginEnableSessionSecure).Language("go"),
	Markdown(`
~SessionSecurer~ helps to ensure user security even in the event of secret leakage. When a user logs in, ~SessionSecurer~ generates a random salt and associates it with the user's record. This salt is then used to sign the user's session token. When the user makes requests to the server, the server verifies that the session token has been signed with the correct salt. If the salt has been changed, the session token is considered invalid and the user is logged out.
    `),
	Markdown(`
## Hooks
[Hooks](https://github.com/qor5/x/blob/8f986dddfeaf235fd42bb3361717551d06695517/login/log_builder.go#L39) are functions that are called before or after certain events.   
The following hooks are available:
### BeforeSetPassword
#### Extra Values
- password

This hook is called before resetting or changing a password. The hook can be used to validate password formats.
### AfterLogin
This hook is called after a successful login.
### AfterFailedToLogin
#### Extra Values
- login error

This hook is called after a failed login. Note that the ~user~ parameter may be nil.
### AfterUserLocked
This hook is called after a user is locked.
### AfterLogout
This hook is called after a logout.
### AfterConfirmSendResetPasswordLink
#### Extra Values
- reset link

This hook is called after confirming the sending of a password reset link. This is where the code to send the reset link to the user should be written.
### AfterResetPassword
This hook is called after a password is reset.
### AfterChangePassword
This hook is called after a password is changed.
### AfterExtendSession
#### Extra Values
- old session token

This hook is called after a session is extended.
### AfterTOTPCodeReused
This hook is called after a TOTP code has been reused.
### AfterOAuthComplete
This hook is called after an OAuth authentication is completed.
    `),
	Markdown(`
## Customize Pages
To customize pages, there are two ways:

1\. Each page has a corresponding ~xxxPageFunc~ to rewrite the page content. You can easily customize a page by copying the [default page func](https://github.com/qor5/admin/blob/main/login/views.go) and modifying it according to your needs.
    `),
	ch.Code(generated.LoginCustomizePage).Language("go"),
	Markdown(`
2\. Only mount the API and serve the login pages manually.  
When you want to embed the login form into an existing page, this way can be very useful.
    `),
	ch.Code(generated.LoginCustomizePage2).Language("go"),
).Slug("basics/login").Title("Login")
View Source
var ManageMenu = Doc(
	Markdown(`
Menu refers to the list on the left side of the page, such as the menu of the Demo below contains Customers and Companies.
`),
	h.Br(),
	utils.Demo("", examples_presets.PresetsDetailPageCardsPath+"/customers", ""),
	Markdown(`
## Menu order
Sorting menus is very simple, use ~MenuOrder~ to sort menus as you want by **slug name** .
`),
	ch.Code(generated.MenuOrderSample).Language("go"),
	utils.DemoWithSnippetLocation("Menu Order", examples.URLPathByFunc(examples_presets.PresetsOrderMenu)+"/books", generated.MenuOrderSampleLocation),
	Markdown(`
## Menu group and icon
~MenuGroup~ can merge multiple items into one group, as shown in the following code.

Use ~MenuIcon~ on ~ModelBuilder~ can set the item icon, and set menu group icon by ~Icon~ following ~MenuGroup~.

Icon strings can be found at <https://fonts.google.com/icons>.
`),
	ch.Code(generated.MenuGroupSample).Language("go"),
	utils.DemoWithSnippetLocation("Menu Group", examples.URLPathByFunc(examples_presets.PresetsGroupMenu)+"/videos", generated.MenuGroupSampleLocation),
).Title("Menu").
	Slug("basics/menu")
View Source
var NotificationCenter = Doc(
	Markdown(`
To enable notification center: Call ~NotificationFunc~ on ~presets.Builder~ With 2 function parameters
like this ~builder.NotificationFunc(NotifierComponent(), NotifierCount())~

The first function is for rendering the content of the popup after user clicked the "bell icon".
The second function is for rendering the number at the top right corner of the "bell icon".
`),

	ch.Code(generated.NotificationCenterSample).Language("go"),
	utils.DemoWithSnippetLocation("Notification Center", examples.URLPathByFunc(examples_presets.PresetsNotificationCenterSample)+"/pages", generated.NotificationCenterSampleLocation),
).Slug("basics/notification-center").Title("Notification Center")
View Source
var Permissions = Doc(
	Markdown(`
QOR5 permission is based on https://github.com/ory/ladon.  
A piece of policy looks like this:  
**Who** is **able** to do **what** on **something** (with given some **context**)  
    `),
	ch.Code(generated.PermissionSyntax).Language("go"),
	Markdown(fmt.Sprintf(`
## Who - Subject
Typically in admin system, they are roles like %s, %s.  
Use %s to fetch current subjects:
    `, "`Admin`", "`Super Admin`", "`SubjectsFunc`")),
	ch.Code(generated.PermissionSubjectsFunc).Language("go"),
	Markdown(fmt.Sprintf(`
## Able - Effect
- %s
- %s

## What - Action
presets has a list of actions:
- %s
- %s
- %s
- %s
- %s

And you can define other specific actions if needed.
## Something - Resource
An arbitrary unique resource name.  
The presets builtin resource format is %s.  
For example %s represents the user record with id 1 under uri user_management.  
Use %s as wildcard.
## Context - Condition
Optional.  
The current context that containing condition information about the resource.  
Use %s to set the context:
    `,
		strings.TrimRight(generated.PermissionAllowed, ","),
		strings.TrimRight(generated.PermissionDenied, ","),
		strings.TrimRight(generated.PermissionPermList, ","),
		strings.TrimRight(generated.PermissionPermGet, ","),
		strings.TrimRight(generated.PermissionPermCreate, ","),
		strings.TrimRight(generated.PermissionPermUpdate, ","),
		strings.TrimRight(generated.PermissionPermDelete, ","),
		"`:presets:mg_menu_group:uri:resource_rn:f_field:`",
		"`:presets:user_management:users:1:`",
		"`*`",
		"`ContextFunc`",
	)),
	ch.Code(generated.PermissionContextFunc).Language("go"),
	Markdown(fmt.Sprintf(`
Policy uses %s to set conditions:  
    `, "`Given`")),
	ch.Code(generated.PermissionGivenFunc).Language("go"),
	Markdown(fmt.Sprintf(`
## Custom Action
Let's say there is a button on User detailing page used to ban the user. And only %s users have permission to execute this action.  
First, create a verifier
    `, "`super_admin`")),
	ch.Code(generated.PermissionNewVerifier).Language("go"),
	Markdown(fmt.Sprintf(`
Then inject this verifier to relevant logic, such as
- whether to show the ban button.
- validate permission before execute the ban action.
    `)),
	ch.Code(generated.PermissionVerifierCheck).Language("go"),
	Markdown(`
Finally, add policy
    `),
	ch.Code(generated.PermissionAddCustomPolicy).Language("go"),
	Markdown(`
## Example
    `),
	ch.Code(generated.PermissionExample).Language("go"),
	Markdown(`
## Debug
    `),
	ch.Code(generated.PermissionVerbose).Language("go"),
	Markdown(`
prints permission logs which is very helpful for debugging the permission policies:
    `),
	ch.Code(`
have permission: true, req: &ladon.Request{Resource:":presets:articles:", Action:"presets:list", Subject:"viewer", Context:ladon.Context(nil)}
have permission: true, req: &ladon.Request{Resource:":presets:articles:articles:1:", Action:"presets:update", Subject:"viewer", Context:ladon.Context(nil)}
have permission: false, req: &ladon.Request{Resource:":presets:articles:articles:2:", Action:"presets:update", Subject:"viewer", Context:ladon.Context(nil)}
    `).Language("plain"),
).Title("Permissions").
	Slug("presets-guide/permissions")
View Source
var PresetsInstantCRUD = Doc(
	Markdown(`
Presets let you config generalized data management UI interface for database.
It's not a scaffolding to generate source code. But provide more abstract and
flexible API to enrich features along the way.

`),
	ch.Code(generated.PresetHelloWorldSample).Language("go"),
	utils.DemoWithSnippetLocation("Presets Hello World", examples.URLPathByFunc(examples_presets.PresetsHelloWorld)+"/customers", generated.PresetHelloWorldSampleLocation),
	Markdown(`
And this ~*presets.Builder~ instance is actually also a ~http.Handler~, So that we can mount it
to the http serve mux directly
`),
	Markdown(`
With ~b.Model(&Customer{})~:

- It setup the global layout with the left navigation menu
- It setup the listing page with a data table
- It add the new button to create a new record
- It setup the editing and creating form as a right side drawer
- It setup each row of data have a operation menu that you have edit and delete operations
- It setup the global search box, can search the model's all string columns
`),
).Title("presets, Instant CRUD").
	Slug("basics/presets-instant-crud")
View Source
var Publish = Doc(
	Markdown(`
Publish controls the online/offline status of records. It generalizes publishing using 3 main modules:
- ~Status~: to flag a record be online/offline
- ~Schedule~: to schedule records to be online/offline automatically
- ~Version~: to allow a record to have more than one copies and chain them together

## Usage
Inject modules to the resource model.
    `),
	ch.Code(generated.PublishInjectModules).Language("go"),
	Markdown(`
Implement primary slug interfaces for passing the values of primary keys between events
    `),
	ch.Code(generated.PublishImplementSlugInterfaces).Language("go"),
	Markdown(`
Create publisher and configure Publish view for model, and remember to display Status and Schedule fields in Editing
    `),
	ch.Code(generated.PublishConfigureView).Language("go"),
	Markdown(`
Implement the publish interfaces if there is a need to publish content to storage(filesystem, AWS S3, ...)
    `),
	ch.Code(generated.PublishImplementPublishInterfaces).Language("go"),
	utils.DemoWithSnippetLocation(
		"Publish",
		path.Join(examples.URLPathByFunc(examples_admin.PublishExample), "/with-publish-products"),
		generated.PublishImplementPublishInterfacesLocation,
	),
	Markdown(fmt.Sprintf(`
## Modules
### Status
%s module stores the status of the record. 
    `, "`Status`")),
	ch.Code(generated.PublishStatus).Language("go"),
	Markdown(`
The initial status is **draft**, after publishing it becomes **online**, and after unpublishing it becomes **offline**.
    `),
	Markdown(fmt.Sprintf(`
### Schedule
%s module schedules records to be online/offline automatically with the publisher job.  
    `, "`Schedule`")),
	ch.Code(generated.PublishSchedule).Language("go"),
	Markdown(fmt.Sprintf(`
If a record has %s set, and the current time is larger than this value, the record will be published and the %s will be set to the actual published time, the %s will be cleared.  
If a record has %s set, and the current time is larger than this value, the record will be unpublished and the %s will be set to the actual unpublished time, the %s will be cleared.  
    `,
		"`ScheduledStartAt`", "`ActualStartAt`", "`ScheduledStartAt`",
		"`ScheduledEndAt`", "`ActualEndAt`", "`ScheduledEndAt`",
	)),
	Markdown(fmt.Sprintf(`
### Version
%s module allows one record to have multiple copies, with Schedule, you can even schedule different prices of a product for a whole year.
    `, "`Version`")),
	ch.Code(generated.PublishVersion).Language("go"),
	Markdown(fmt.Sprintf(`
The %s will be the primary key. By default, the %s value will be %s, e.g. %s. And you can rename a version on interface, which will modify the value of %s.   
    `, "`Version`", "`Version`", "`YYYY-MM-DD-vSeq`", "`2006-01-02-v01`", "`VersionName`")),
	Markdown(fmt.Sprintf(`
### List
%s module publishes list page of resource.
    `, "`List`")),
	ch.Code(generated.PublishList).Language("go"),
).Slug("basics/publish").Title("Publish")
View Source
var Role = Doc(
	Markdown(`
**Role** provides a UI interface to manage roles(subjects) and their permissions.  

1\.  enable permission DBPolicy
    `),
	ch.Code(generated.RolePermEnableDBPolicy).Language("go"),
	Markdown(`
2\. configure role  
set resources that you want to manage on interface
    `),
	ch.Code(generated.RoleSetResources).Language("go"),
	Markdown(`
(optional) set actions, the default value is the following 
    `),
	ch.Code(generated.RoleSetActions).Language("go"),
	Markdown(`
(optional) set editor subject to set who can edit **Role**
    `),
	ch.Code(generated.RoleSetEditorSubject).Language("go"),
	Markdown(`
attach role to presets builder
    `),
	ch.Code(generated.RoleAttachToPresetsBuilder).Language("go"),
).Title("Role").
	Slug("presets-guide/role")
View Source
var SEO = Doc(
	Markdown(`
## Introduction

The seo package allows for the management and injection of dynamic data into HTML tags for the purpose of Search Engine Optimization.

## Create a SEO builder

- Create a builder and register global seo by default. ~~db~~ is an instance of gorm.DB

  ~~~go
  builder := seo.NewBuilder(db)
  ~~~

- The default global seo name is ~~Global SEO~~, if you want to customize the name, passing the name through ~~WithGlobalSEOName~~ option to the ~~NewBuilder~~ function.

  ~~~go
  builder := seo.NewBuilder(db, seo.WithGlobalSEOName("My Global SEO"))
  ~~~

- Turn off the default inherit the upper level SEO data when the current SEO data is missing

  ~~~go
  builder := seo.NewBuilder(db, seo.WithInherited(false))
  ~~~

- The seo package supports localization. you can pass the localization code you want to support through the ~~WithLocales~~ option to the ~~NewBuilder~~ function. if no localization is set, it defaults to empty.

  ~~~go
  builder := seo.NewBuilder(db, seo.WithLocales('zh', 'en', 'jp'))
  ~~~

- **You can pass multiple options at once**

  ~~~go
  builder := seo.NewBuilder(db, seo.WithLocales('zh'), seo.WithInherited(false), seo.WithGlobalSEOName("My Global SEO"))
  ~~~

## Register and remove SEO

All registered SEO names are unique, If you have already registered a SEO named ~~Test~~, attempting to register SEO with the same name ~~Test~~ will cause the program to panic.

The second parameter named ~~model~~ in the ~~RegisterSEO(name string, model ...interface{})~~ method is an
instance of a type that has a field of type ~~Setting~~. If you pass a  ~~model~~ whose type
does not have such a field, the program will panic.

For Example:

~~~go
builder.RegisterSEO("Test")
type Test struct {
    ...
    // doesn't have a field of type Setting
}
builder.RegisterSEO("Test", &Test{}) // will panic
~~~

There are two types of SEOs, one is SEO with model, the other is SEO without model.
if you want to register a no model SEO, you can call RegisterSEO method like this:

~~~go
seoBuilder.RegisterSEO("About Us")
~~~

if you want to register a SEO with model, you can call RegisterSEO method like this:

~~~go
seoBuilder.RegisterSEO("Product", &Product{})
~~~

- Remove a SEO

  **NOTE: The global seo cannot be removed**

  ~~~go
  builder.RemoveSEO(&Product{}).RemoveSEO("Not Found")
  ~~~

  When you remove an SEO, the new parent of its child SEO becomes the parent of the seo.

- The seo supports hierarchy, and you can use the ~~SetParent~~ or ~~AppendChild~~ methods of ~~SEO~~ to configure the hierarchy.

  **With ~~AppendChildren~~ method**

  ~~~go
  builder.RegisterSEO("A").AppendChildren(
      builder.RegisterSEO("B"),
      builder.RegisterSEO("C"),
      builder.RegisterSEO("D"),
  )
  ~~~

  **With ~~SetParent~~ Method**

  ~~~go
  seoA := builder.RegisterSEO("A")
  builder.RegisterSEO("B").SetParent(seoA)
  builder.RegisterSEO("C").SetParent(seoA)
  builder.RegisterSEO("D").SetParent(seoA)
  ~~~

  The final seo structure is as follows:

  ~~~txt
  Global SEO
  ├── A
      ├── B
      └── C
      └── D
  ~~~

## Configure SEO

- Register setting variables

  ~~~go
  builder := NewBuilder(dbForTest)
  builder.RegisterSEO(&Product{}).
    RegisterSettingVariables("Type", "Place of Origin")
  ~~~

- Register context variables

  ~~~go
  seoBuilder = seo.NewBuilder(db)
  seoBuilder.RegisterSEO(&models.Post{}).
      RegisterContextVariable(
          "Title",
          func(object interface{}, _ *seo.Setting, _ *http.Request) string {
              if article, ok := object.(models.Post); ok {
                  return article.Title
              }
              return ""
          },
      )
  ~~~

- Register Meta Property

  ~~~go
  seoBuilder = seo.NewBuilder(db)
  seoBuilder.RegisterSEO(&models.Post{}).
      RegisterMetaProperty(
          "og:audio",
          func(object interface{}, _ *seo.Setting, _ *http.Request) string {
              if article, ok := object.(models.Post); ok {
                  return article.audio
              }
              return ""
          },
      )
  ~~~

## Render SEO html data

### Render a single seo

#### Render a single seo with model

  To call ~~Render(obj interface{}, req *http.Request)~~ for rendering a single seo.

  NOTE: ~~obj~~ must be of type ~~*NameObj~~ or a pointer to a struct that has a field of type ~~Setting~~.

  ~~~go
  type Post struct {
      Title   string
      Author  string
      Content string
      SEO     Setting
  }

  post := &Post{
      Title:   "TestRender",
      Author:  "iBakuman",
      Content: "Hello, Qor5 SEO",
      SEO: Setting{
          Title:            "{.{Title}}",
          Description:      "post for testing",
          EnabledCustomize: true,
      },
  }
  builder := NewBuilder(dbForTest)
  builder.RegisterSEO(&Post{}).RegisterContextVariable(
      "Title",
      func(post interface{}, _ *Setting, _ *http.Request) string {
          if p, ok := post.(*Post); ok {
              return p.Title
          }
          return "No title"
      },
  ).RegisterMetaProperty("og:title",
      func(post interface{}, _ *Setting, _ *http.Request) string {
          return "Title for og:title"
      },
  )

  defaultReq, _ := http.NewRequest("POST", "http://www.demo.qor5.com", nil)
  res, err := builder.Render(post, defaultReq).MarshalHTML(context.TODO())
  if err != nil {
      panic(err)
  }
  fmt.Println(string(res))
  ~~~

  The output of the above code is as follows:

  ~~~txt
  <title>TestRender</title>

  <meta name='description' content='post for testing'>

  <meta name='keywords'>

  <meta property='og:description' name='og:description'>

  <meta property='og:url' name='og:url'>

  <meta property='og:type' name='og:type' content='website'>

  <meta property='og:image' name='og:image'>

  <meta property='og:title' name='og:title' content='Title for og:title'>
  ~~~

#### Render a single seo without model

  ~~~go
    seoBuilder.Render(&NameObj{"About US", Locale: "en"})
  ~~~

### Render multiple SEOs at once

#### Render multiple SEOs with model

  To call ~~BatchRender(objs []interface, req *http.Request)~~ for batch rendering.

  **NOTE: You need to ensure that all elements in ~~objs~~ are of the same type.**

  ~~BatchRender~~ does not check if all elements in ~~objs~~ are of the same type, as this can be performance-intensive. Therefore,
  it is the responsibility of the caller to ensure that all elements in ~~objs~~ are of the same type.
  If you pass ~~objs~~ with various types of SEO, it will only take the type of the first element as the standard to obtain the SEO configuration used in the rendering process.

  ~~~go
  type Post struct {
      Title   string
      Author  string
      Content string
      SEO     Setting
      l10n.Locale
  }

  posts := []interface{}{
      &Post{
          Title:   "TestRenderA",
          Author:  "iBakuman",
          Content: "Hello, Qor5 SEO",
          SEO: Setting{
              Title:            "{.{Title}}",
              Description:      "postA for testing",
              EnabledCustomize: true,
          },
      },
      &Post{
          Title:   "TestB",
          Author:  "iBakuman",
          Content: "Hello, Qor5 SEO",
          SEO: Setting{
              Title:            "{.{Title}}",
              Description:      "postB for testing",
              EnabledCustomize: true,
          },
      },
  }
  builder := NewBuilder(dbForTest, WithLocales("en"))
  builder.RegisterSEO(&Post{}).RegisterContextVariable(
      "Title",
      func(post interface{}, _ *Setting, _ *http.Request) string {
          if p, ok := post.(*Post); ok {
              return p.Title
          }
          return "No title"
      },
  ).RegisterMetaProperty("og:title",
      func(post interface{}, _ *Setting, _ *http.Request) string {
          return "Title for og:title"
      },
  )

  defaultReq, _ := http.NewRequest("POST", "http://www.demo.qor5.com", nil)
  SEOs := builder.BatchRender(posts, defaultReq)
  for _, seo := range SEOs {
      html, _ := seo.MarshalHTML(context.TODO())
      fmt.Println(string(html))
      fmt.Println("----------------------------")
  }
  ~~~

  The output of the above code is as follows:

  ~~~txt
  <title>TestRenderA</title>

  <meta name='description' content='postA for testing'>

  <meta name='keywords'>

  <meta property='og:description' name='og:description'>

  <meta property='og:url' name='og:url'>

  <meta property='og:type' name='og:type' content='website'>

  <meta property='og:image' name='og:image'>

  <meta property='og:title' name='og:title' content='Title for og:title'>

  ----------------------------

  <title>TestB</title>

  <meta name='description' content='postB for testing'>

  <meta name='keywords'>

  <meta property='og:url' name='og:url'>

  <meta property='og:type' name='og:type' content='website'>

  <meta property='og:image' name='og:image'>

  <meta property='og:title' name='og:title' content='Title for og:title'>

  <meta property='og:description' name='og:description'>

  ----------------------------
  ~~~

#### Render multiple SEOs without model

  ~~~go
  seoBuilder.BatchRender(NewNonModelSEOSlice("Product", "en", "zh"))
  ~~~
`)).Title("SEO")
View Source
var ShortCut = Doc(
	Markdown(`
To add keyboard shortcut to a button:

Trigger the event by [GlobalEvents](https://www.npmjs.com/package/vue-global-events).
You can configure your own keyboard event like ~@keydown.enter~ to trigger the event.

Also you can setup the ~filter~ function to limit when this event can be triggered by shortcut.
In the example, the event would only be triggered when ~locals.shortCutEnabled~ is opened.
`),

	ch.Code(generated.ShortCutSample).Language("go"),
	utils.DemoWithSnippetLocation("Shortcut", examples_web.ShortCutSamplePath, generated.ShortCutSampleLocation),
).Slug("basics/shortcut").Title("Keyboard Shortcut")
View Source
var Slug = Doc(
	Markdown(`
Slug provides an easy way to create pretty URLs for your model.
## Usage

If the source field called ~~Name~~, Use ~~*WithSlug~~ which is ~~NameWithSlug~~ as the slug field name, the field type should be ~~slug.Slug~~. Then the pretty URL would be derived from ~~Name~~ automatically on editing.
~~~go

type User struct {
	gorm.Model
	Name            string
	NameWithSlug    slug.Slug
}
~~~
`),
).Title("Slug")
View Source
var VariantSubForm = Doc(
	Markdown(`
VSelect changes, the form below it will change to a new form accordingly.

By use of ~web.Portal()~ and ~VSelect~'s ~OnInput~
`),
	ch.Code(generated.VuetifyVariantSubForm).Language("go"),
	utils.DemoWithSnippetLocation("Vuetify Variant Sub Form", examples_vuetify.VuetifyVariantSubFormPath, generated.VuetifyVariantSubFormLocation),
).Title("Variant Sub Form").
	Slug("vuetify-components/variant-sub-form")
View Source
var Worker = Doc(
	Markdown(fmt.Sprintf(`
Worker runs a single Job in the background, it can do so immediately or at a scheduled time.  
Once registered with QOR Admin, Worker will provide a Workers section in the navigation tree, containing pages for listing and managing the following aspects of Workers:

- All Jobs.
- Running: Jobs that are currently running.
- Scheduled: Jobs which have been scheduled to run at a time in the future.
- Done: finished Jobs.
- Errors: any errors output from any Workers that have been run.

## Note
- The default que GoQueQueue(https://github.com/tnclong/go-que) only supports postgres for now.
- To make a job abortable, you need to check %s channel in job handler and stop the handler func.
    `, "`ctx.Done()`")),
	Markdown(`
## Example
`),
	ch.Code(generated.WorkerExample).Language("go"),
	utils.DemoWithSnippetLocation(
		"Worker",
		path.Join(examples.URLPathByFunc(examples_admin.WorkerExample), "/workers"),
		generated.WorkerExampleLocation,
	),
	Markdown(`
## Action Worker
Action Worker is used to visualize the progress of long-running actions.
    `),
	ch.Code(generated.ActionWorkerExample).Language("go"),
	utils.DemoWithSnippetLocation(
		"Action Worker",
		path.Join(examples.URLPathByFunc(examples_admin.ActionWorkerExample), "/example-resources"),
		generated.ActionWorkerExampleLocation,
	),
).Slug("basics/worker").Title("Worker")

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL