V3
This commit is contained in:
parent
f0df90f9b7
commit
407eb21fc4
@ -70,19 +70,6 @@ module.exports = class ArticlesDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
await articles.setAffiliate_links(data.affiliate_links || [], {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await articles.setMedia_assets(data.media_assets || [], {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await articles.setArticle_bundle_items(data.article_bundle_items || [], {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
await FileDBApi.replaceRelationFiles(
|
||||||
{
|
{
|
||||||
@ -214,25 +201,9 @@ module.exports = class ArticlesDBApi {
|
|||||||
|
|
||||||
await articles.update(updatePayload, {transaction});
|
await articles.update(updatePayload, {transaction});
|
||||||
|
|
||||||
|
// Article source-content updates intentionally skip generated many-to-many
|
||||||
|
// affiliate/media/bundle sync. Those joins are handled later in Export Studio.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (data.affiliate_links !== undefined) {
|
|
||||||
await articles.setAffiliate_links(data.affiliate_links, { transaction });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.media_assets !== undefined) {
|
|
||||||
await articles.setMedia_assets(data.media_assets, { transaction });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.article_bundle_items !== undefined) {
|
|
||||||
await articles.setArticle_bundle_items(data.article_bundle_items, { transaction });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await FileDBApi.replaceRelationFiles(
|
await FileDBApi.replaceRelationFiles(
|
||||||
{
|
{
|
||||||
belongsTo: db.articles.getTableName(),
|
belongsTo: db.articles.getTableName(),
|
||||||
@ -347,22 +318,6 @@ module.exports = class ArticlesDBApi {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
output.affiliate_links = await articles.getAffiliate_links({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
output.media_assets = await articles.getMedia_assets({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
output.article_bundle_items = await articles.getArticle_bundle_items({
|
|
||||||
transaction
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,32 +337,10 @@ module.exports = class ArticlesDBApi {
|
|||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
model: db.affiliate_links,
|
|
||||||
as: 'affiliate_links',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
model: db.media_assets,
|
|
||||||
as: 'media_assets',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
model: db.article_bundle_items,
|
|
||||||
as: 'article_bundle_items',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
model: db.file,
|
model: db.file,
|
||||||
as: 'featured_images',
|
as: 'featured_images',
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@ -533,76 +466,6 @@ module.exports = class ArticlesDBApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.affiliate_links) {
|
|
||||||
const searchTerms = filter.affiliate_links.split('|');
|
|
||||||
|
|
||||||
include = [
|
|
||||||
{
|
|
||||||
model: db.affiliate_links,
|
|
||||||
as: 'affiliate_links_filter',
|
|
||||||
required: searchTerms.length > 0,
|
|
||||||
where: searchTerms.length > 0 ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
original_url: {
|
|
||||||
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} : undefined
|
|
||||||
},
|
|
||||||
...include,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.media_assets) {
|
|
||||||
const searchTerms = filter.media_assets.split('|');
|
|
||||||
|
|
||||||
include = [
|
|
||||||
{
|
|
||||||
model: db.media_assets,
|
|
||||||
as: 'media_assets_filter',
|
|
||||||
required: searchTerms.length > 0,
|
|
||||||
where: searchTerms.length > 0 ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
file_name: {
|
|
||||||
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} : undefined
|
|
||||||
},
|
|
||||||
...include,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.article_bundle_items) {
|
|
||||||
const searchTerms = filter.article_bundle_items.split('|');
|
|
||||||
|
|
||||||
include = [
|
|
||||||
{
|
|
||||||
model: db.article_bundle_items,
|
|
||||||
as: 'article_bundle_items_filter',
|
|
||||||
required: searchTerms.length > 0,
|
|
||||||
where: searchTerms.length > 0 ? {
|
|
||||||
[Op.or]: [
|
|
||||||
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
|
|
||||||
{
|
|
||||||
sort_order: {
|
|
||||||
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} : undefined
|
|
||||||
},
|
|
||||||
...include,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (filter.createdAtRange) {
|
if (filter.createdAtRange) {
|
||||||
const [start, end] = filter.createdAtRange;
|
const [start, end] = filter.createdAtRange;
|
||||||
|
|
||||||
|
|||||||
@ -1,272 +1,91 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import BaseIcon from '../BaseIcon';
|
import { GridRowParams } from '@mui/x-data-grid'
|
||||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
import ListActionsPopover from '../ListActionsPopover'
|
||||||
import axios from 'axios';
|
import { hasPermission } from '../../helpers/userPermissions'
|
||||||
import {
|
|
||||||
GridActionsCellItem,
|
|
||||||
GridRowParams,
|
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
|
||||||
import ImageField from '../ImageField';
|
|
||||||
import {saveFile} from "../../helpers/fileSaver";
|
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
type Params = (id: string) => void
|
||||||
|
|
||||||
type Params = (id: string) => void;
|
export const loadColumns = async (onDelete: Params, _entityName: string, user) => {
|
||||||
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_ARTICLES')
|
||||||
|
|
||||||
export const loadColumns = async (
|
return [
|
||||||
onDelete: Params,
|
{
|
||||||
entityName: string,
|
field: 'title',
|
||||||
|
headerName: 'Title',
|
||||||
user
|
flex: 1,
|
||||||
|
minWidth: 180,
|
||||||
) => {
|
filterable: false,
|
||||||
async function callOptionsApi(entityName: string) {
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
|
editable: hasUpdatePermission,
|
||||||
|
},
|
||||||
try {
|
{
|
||||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
field: 'status',
|
||||||
return data.data;
|
headerName: 'Status',
|
||||||
} catch (error) {
|
flex: 0.6,
|
||||||
console.log(error);
|
minWidth: 120,
|
||||||
return [];
|
filterable: false,
|
||||||
}
|
headerClassName: 'datagrid--header',
|
||||||
}
|
cellClassName: 'datagrid--cell',
|
||||||
|
editable: hasUpdatePermission,
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_ARTICLES')
|
},
|
||||||
|
{
|
||||||
return [
|
field: 'excerpt',
|
||||||
|
headerName: 'Excerpt',
|
||||||
{
|
flex: 1.2,
|
||||||
field: 'title',
|
minWidth: 220,
|
||||||
headerName: 'Title',
|
filterable: false,
|
||||||
flex: 1,
|
headerClassName: 'datagrid--header',
|
||||||
minWidth: 120,
|
cellClassName: 'datagrid--cell',
|
||||||
filterable: false,
|
editable: hasUpdatePermission,
|
||||||
headerClassName: 'datagrid--header',
|
},
|
||||||
cellClassName: 'datagrid--cell',
|
{
|
||||||
|
field: 'content_html',
|
||||||
|
headerName: 'HTML Content',
|
||||||
editable: hasUpdatePermission,
|
flex: 1.2,
|
||||||
|
minWidth: 220,
|
||||||
|
filterable: false,
|
||||||
},
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
{
|
editable: hasUpdatePermission,
|
||||||
field: 'slug',
|
},
|
||||||
headerName: 'Slug',
|
{
|
||||||
flex: 1,
|
field: 'content_markdown',
|
||||||
minWidth: 120,
|
headerName: 'Meta Paste',
|
||||||
filterable: false,
|
flex: 1,
|
||||||
headerClassName: 'datagrid--header',
|
minWidth: 180,
|
||||||
cellClassName: 'datagrid--cell',
|
filterable: false,
|
||||||
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
},
|
||||||
|
{
|
||||||
},
|
field: 'source_url',
|
||||||
|
headerName: 'Source URL',
|
||||||
{
|
flex: 1,
|
||||||
field: 'status',
|
minWidth: 180,
|
||||||
headerName: 'Status',
|
filterable: false,
|
||||||
flex: 1,
|
headerClassName: 'datagrid--header',
|
||||||
minWidth: 120,
|
cellClassName: 'datagrid--cell',
|
||||||
filterable: false,
|
editable: hasUpdatePermission,
|
||||||
headerClassName: 'datagrid--header',
|
},
|
||||||
cellClassName: 'datagrid--cell',
|
{
|
||||||
|
field: 'actions',
|
||||||
|
type: 'actions',
|
||||||
editable: hasUpdatePermission,
|
minWidth: 30,
|
||||||
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
},
|
getActions: (params: GridRowParams) => [
|
||||||
|
<div key={params?.row?.id}>
|
||||||
{
|
<ListActionsPopover
|
||||||
field: 'excerpt',
|
onDelete={onDelete}
|
||||||
headerName: 'Excerpt',
|
itemId={params?.row?.id}
|
||||||
flex: 1,
|
pathEdit={`/articles/articles-edit/?id=${params?.row?.id}`}
|
||||||
minWidth: 120,
|
pathView={`/articles/articles-view/?id=${params?.row?.id}`}
|
||||||
filterable: false,
|
hasUpdatePermission={hasUpdatePermission}
|
||||||
headerClassName: 'datagrid--header',
|
/>
|
||||||
cellClassName: 'datagrid--cell',
|
</div>,
|
||||||
|
],
|
||||||
|
},
|
||||||
editable: hasUpdatePermission,
|
]
|
||||||
|
}
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'content_html',
|
|
||||||
headerName: 'ContentHTML',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'content_markdown',
|
|
||||||
headerName: 'ContentMarkdown',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'featured_images',
|
|
||||||
headerName: 'FeaturedImages',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
editable: false,
|
|
||||||
sortable: false,
|
|
||||||
renderCell: (params: GridValueGetterParams) => (
|
|
||||||
<ImageField
|
|
||||||
name={'Avatar'}
|
|
||||||
image={params?.row?.featured_images}
|
|
||||||
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'source_url',
|
|
||||||
headerName: 'SourceURL',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'published_at',
|
|
||||||
headerName: 'PublishedAt',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
|
|
||||||
type: 'dateTime',
|
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
|
||||||
new Date(params.row.published_at),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'affiliate_links',
|
|
||||||
headerName: 'AffiliateLinks',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
editable: false,
|
|
||||||
sortable: false,
|
|
||||||
type: 'singleSelect',
|
|
||||||
valueFormatter: ({ value }) =>
|
|
||||||
dataFormatter.affiliate_linksManyListFormatter(value).join(', '),
|
|
||||||
renderEditCell: (params) => (
|
|
||||||
<DataGridMultiSelect {...params} entityName={'affiliate_links'}/>
|
|
||||||
),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'media_assets',
|
|
||||||
headerName: 'MediaAssets',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
editable: false,
|
|
||||||
sortable: false,
|
|
||||||
type: 'singleSelect',
|
|
||||||
valueFormatter: ({ value }) =>
|
|
||||||
dataFormatter.media_assetsManyListFormatter(value).join(', '),
|
|
||||||
renderEditCell: (params) => (
|
|
||||||
<DataGridMultiSelect {...params} entityName={'media_assets'}/>
|
|
||||||
),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'article_bundle_items',
|
|
||||||
headerName: 'ArticleBundleItems',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
editable: false,
|
|
||||||
sortable: false,
|
|
||||||
type: 'singleSelect',
|
|
||||||
valueFormatter: ({ value }) =>
|
|
||||||
dataFormatter.article_bundle_itemsManyListFormatter(value).join(', '),
|
|
||||||
renderEditCell: (params) => (
|
|
||||||
<DataGridMultiSelect {...params} entityName={'article_bundle_items'}/>
|
|
||||||
),
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'actions',
|
|
||||||
type: 'actions',
|
|
||||||
minWidth: 30,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
getActions: (params: GridRowParams) => {
|
|
||||||
|
|
||||||
return [
|
|
||||||
<div key={params?.row?.id}>
|
|
||||||
<ListActionsPopover
|
|
||||||
onDelete={onDelete}
|
|
||||||
itemId={params?.row?.id}
|
|
||||||
pathEdit={`/articles/articles-edit/?id=${params?.row?.id}`}
|
|
||||||
pathView={`/articles/articles-view/?id=${params?.row?.id}`}
|
|
||||||
|
|
||||||
hasUpdatePermission={hasUpdatePermission}
|
|
||||||
|
|
||||||
/>
|
|
||||||
</div>,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -34,13 +34,13 @@ const ArticlesTablesPage = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
|
||||||
const [filters] = useState([{label: 'Title', title: 'title'},{label: 'Slug', title: 'slug'},{label: 'Excerpt', title: 'excerpt'},{label: 'ContentHTML', title: 'content_html'},{label: 'ContentMarkdown', title: 'content_markdown'},{label: 'SourceURL', title: 'source_url'},
|
const [filters] = useState([
|
||||||
|
{ label: 'Title', title: 'title' },
|
||||||
|
{ label: 'Status', title: 'status', type: 'enum', options: ['draft', 'published', 'archived'] },
|
||||||
{label: 'PublishedAt', title: 'published_at', date: 'true'},
|
{ label: 'Excerpt', title: 'excerpt' },
|
||||||
|
{ label: 'HTML Content', title: 'content_html' },
|
||||||
{label: 'AffiliateLinks', title: 'affiliate_links'},{label: 'MediaAssets', title: 'media_assets'},{label: 'ArticleBundleItems', title: 'article_bundle_items'},
|
{ label: 'Meta Paste', title: 'content_markdown' },
|
||||||
{label: 'Status', title: 'status', type: 'enum', options: ['draft','published','archived']},
|
{ label: 'Source URL', title: 'source_url' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ARTICLES');
|
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ARTICLES');
|
||||||
@ -94,7 +94,7 @@ const ArticlesTablesPage = () => {
|
|||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||||
|
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Item'/>}
|
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Article'/>}
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
className={'mr-3'}
|
className={'mr-3'}
|
||||||
|
|||||||
@ -1,691 +1,128 @@
|
|||||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement, useState } from 'react'
|
||||||
|
import { Field, Form, Formik } from 'formik'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
import BaseButton from '../../components/BaseButton'
|
||||||
|
import BaseButtons from '../../components/BaseButtons'
|
||||||
|
import BaseDivider from '../../components/BaseDivider'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import FormField from '../../components/FormField'
|
||||||
|
import { RichTextField } from '../../components/RichTextField'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||||
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import { getPageTitle } from '../../config'
|
import { getPageTitle } from '../../config'
|
||||||
|
|
||||||
import { Field, Form, Formik } from 'formik'
|
|
||||||
import FormField from '../../components/FormField'
|
|
||||||
import BaseDivider from '../../components/BaseDivider'
|
|
||||||
import BaseButtons from '../../components/BaseButtons'
|
|
||||||
import BaseButton from '../../components/BaseButton'
|
|
||||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
|
||||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
|
||||||
import FormFilePicker from '../../components/FormFilePicker'
|
|
||||||
import FormImagePicker from '../../components/FormImagePicker'
|
|
||||||
import { SwitchField } from '../../components/SwitchField'
|
|
||||||
|
|
||||||
import { SelectField } from '../../components/SelectField'
|
|
||||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
|
||||||
import {RichTextField} from "../../components/RichTextField";
|
|
||||||
|
|
||||||
import { create } from '../../stores/articles/articlesSlice'
|
import { create } from '../../stores/articles/articlesSlice'
|
||||||
import { useAppDispatch } from '../../stores/hooks'
|
import { useAppDispatch } from '../../stores/hooks'
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
title: '',
|
||||||
|
status: 'draft',
|
||||||
title: '',
|
excerpt: '',
|
||||||
|
content_html: '',
|
||||||
|
content_markdown: '',
|
||||||
|
source_url: '',
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
slug: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
status: 'draft',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
excerpt: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
content_html: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
content_markdown: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
featured_images: [],
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
source_url: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
published_at: '',
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
affiliate_links: [],
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
media_assets: [],
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
article_bundle_items: [],
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (error: any) => {
|
||||||
|
if (!error) return 'Article could not be saved. Please check the form and try again.'
|
||||||
|
if (typeof error === 'string') return error
|
||||||
|
if (error.message) return error.message
|
||||||
|
if (error.error) return error.error
|
||||||
|
return 'Article could not be saved. Please check the form and try again.'
|
||||||
|
}
|
||||||
|
|
||||||
const ArticlesNew = () => {
|
const ArticlesNew = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const [submitError, setSubmitError] = useState('')
|
||||||
|
|
||||||
|
const handleSubmit = async (data, { setSubmitting }) => {
|
||||||
|
setSubmitError('')
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
try {
|
||||||
await dispatch(create(data))
|
await dispatch(create(data)).unwrap()
|
||||||
await router.push('/articles/articles-list')
|
await router.push('/articles/articles-list')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Article create failed:', error)
|
||||||
|
setSubmitError(getErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('New Item')}</title>
|
<title>{getPageTitle('New Article')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='New Article' main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik
|
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
initialValues={
|
{({ isSubmitting }) => (
|
||||||
|
<Form>
|
||||||
initialValues
|
<p className='mb-4 text-sm text-gray-600 dark:text-dark-600'>
|
||||||
|
Add the source article here. Buyer-specific affiliate link replacement happens later in Export Studio.
|
||||||
}
|
</p>
|
||||||
onSubmit={(values) => handleSubmit(values)}
|
|
||||||
>
|
{submitError && (
|
||||||
<Form>
|
<div className='mb-4 rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700'>
|
||||||
|
{submitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<FormField
|
|
||||||
label="Title"
|
<FormField label='Title'>
|
||||||
>
|
<Field name='title' placeholder='Article title' />
|
||||||
<Field
|
</FormField>
|
||||||
name="title"
|
|
||||||
placeholder="Title"
|
<FormField label='Status' labelFor='status'>
|
||||||
/>
|
<Field name='status' id='status' component='select'>
|
||||||
</FormField>
|
<option value='draft'>Draft</option>
|
||||||
|
<option value='published'>Published</option>
|
||||||
|
<option value='archived'>Archived</option>
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Excerpt' hasTextareaHeight>
|
||||||
|
<Field name='excerpt' as='textarea' placeholder='Optional short summary for the article list/export screen' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='HTML Content' hasTextareaHeight>
|
||||||
|
<Field name='content_html' id='content_html' component={RichTextField} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Meta Paste' hasTextareaHeight>
|
||||||
|
<Field
|
||||||
|
name='content_markdown'
|
||||||
|
as='textarea'
|
||||||
|
placeholder='Paste your metadata, notes, markdown, or source details here'
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label='Source URL'>
|
||||||
|
<Field name='source_url' placeholder='Optional source URL' />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
<BaseButtons>
|
||||||
<FormField
|
<BaseButton type='submit' color='info' label={isSubmitting ? 'Saving...' : 'Save Article'} disabled={isSubmitting} />
|
||||||
label="Slug"
|
<BaseButton type='reset' color='info' outline label='Reset' disabled={isSubmitting} />
|
||||||
>
|
<BaseButton
|
||||||
<Field
|
type='button'
|
||||||
name="slug"
|
color='danger'
|
||||||
placeholder="Slug"
|
outline
|
||||||
/>
|
label='Cancel'
|
||||||
</FormField>
|
onClick={() => router.push('/articles/articles-list')}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Status" labelFor="status">
|
|
||||||
<Field name="status" id="status" component="select">
|
|
||||||
|
|
||||||
<option value="draft">draft</option>
|
|
||||||
|
|
||||||
<option value="published">published</option>
|
|
||||||
|
|
||||||
<option value="archived">archived</option>
|
|
||||||
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Excerpt" hasTextareaHeight>
|
|
||||||
<Field name="excerpt" as="textarea" placeholder="Excerpt" />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='ContentHTML' hasTextareaHeight>
|
|
||||||
<Field
|
|
||||||
name='content_html'
|
|
||||||
id='content_html'
|
|
||||||
component={RichTextField}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="ContentMarkdown" hasTextareaHeight>
|
|
||||||
<Field name="content_markdown" as="textarea" placeholder="ContentMarkdown" />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField>
|
|
||||||
<Field
|
|
||||||
label='FeaturedImages'
|
|
||||||
color='info'
|
|
||||||
icon={mdiUpload}
|
|
||||||
path={'articles/featured_images'}
|
|
||||||
name='featured_images'
|
|
||||||
id='featured_images'
|
|
||||||
schema={{
|
|
||||||
size: undefined,
|
|
||||||
formats: undefined,
|
|
||||||
}}
|
|
||||||
component={FormImagePicker}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="SourceURL"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="source_url"
|
|
||||||
placeholder="SourceURL"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label="PublishedAt"
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
type="datetime-local"
|
|
||||||
name="published_at"
|
|
||||||
placeholder="PublishedAt"
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='AffiliateLinks' labelFor='affiliate_links'>
|
|
||||||
<Field
|
|
||||||
name='affiliate_links'
|
|
||||||
id='affiliate_links'
|
|
||||||
itemRef={'affiliate_links'}
|
|
||||||
options={[]}
|
|
||||||
component={SelectFieldMany}>
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='MediaAssets' labelFor='media_assets'>
|
|
||||||
<Field
|
|
||||||
name='media_assets'
|
|
||||||
id='media_assets'
|
|
||||||
itemRef={'media_assets'}
|
|
||||||
options={[]}
|
|
||||||
component={SelectFieldMany}>
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label='ArticleBundleItems' labelFor='article_bundle_items'>
|
|
||||||
<Field
|
|
||||||
name='article_bundle_items'
|
|
||||||
id='article_bundle_items'
|
|
||||||
itemRef={'article_bundle_items'}
|
|
||||||
options={[]}
|
|
||||||
component={SelectFieldMany}>
|
|
||||||
</Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BaseDivider />
|
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
|
||||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/articles/articles-list')}/>
|
|
||||||
</BaseButtons>
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
@ -694,15 +131,7 @@ const ArticlesNew = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ArticlesNew.getLayout = function getLayout(page: ReactElement) {
|
ArticlesNew.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission='CREATE_ARTICLES'>{page}</LayoutAuthenticated>
|
||||||
<LayoutAuthenticated
|
|
||||||
|
|
||||||
permission={'CREATE_ARTICLES'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</LayoutAuthenticated>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ArticlesNew
|
export default ArticlesNew
|
||||||
|
|||||||
@ -34,13 +34,13 @@ const ArticlesTablesPage = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
|
||||||
const [filters] = useState([{label: 'Title', title: 'title'},{label: 'Slug', title: 'slug'},{label: 'Excerpt', title: 'excerpt'},{label: 'ContentHTML', title: 'content_html'},{label: 'ContentMarkdown', title: 'content_markdown'},{label: 'SourceURL', title: 'source_url'},
|
const [filters] = useState([
|
||||||
|
{ label: 'Title', title: 'title' },
|
||||||
|
{ label: 'Status', title: 'status', type: 'enum', options: ['draft', 'published', 'archived'] },
|
||||||
{label: 'PublishedAt', title: 'published_at', date: 'true'},
|
{ label: 'Excerpt', title: 'excerpt' },
|
||||||
|
{ label: 'HTML Content', title: 'content_html' },
|
||||||
{label: 'AffiliateLinks', title: 'affiliate_links'},{label: 'MediaAssets', title: 'media_assets'},{label: 'ArticleBundleItems', title: 'article_bundle_items'},
|
{ label: 'Meta Paste', title: 'content_markdown' },
|
||||||
{label: 'Status', title: 'status', type: 'enum', options: ['draft','published','archived']},
|
{ label: 'Source URL', title: 'source_url' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ARTICLES');
|
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ARTICLES');
|
||||||
@ -94,7 +94,7 @@ const ArticlesTablesPage = () => {
|
|||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||||
|
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Item'/>}
|
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Article'/>}
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
className={'mr-3'}
|
className={'mr-3'}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user