@raketa-cloud/admin 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cloud/Categories.js +97 -0
- package/src/cloud/documents/categoryDocuments.js +2 -14
- package/src/handlers/categories/index.js +122 -0
- package/src/helpers/Inflector.js +3 -0
- package/src/index.js +1 -1
- package/src/routeCategoriesModule.js +16 -0
- package/src/setupCloud.js +2 -0
- package/src/tests/handlers/categories/create.test.js +94 -0
- package/src/tests/handlers/categories/create_form.test.js +26 -0
- package/src/tests/handlers/categories/destroy.test.js +53 -0
- package/src/tests/handlers/categories/edit_form.test.js +59 -0
- package/src/tests/handlers/categories/index.test.js +46 -0
- package/src/tests/handlers/categories/update.test.js +88 -0
- package/src/tests/support/factory.js +8 -0
- package/src/views/articles/create_form.ejs +1 -1
- package/src/views/articles/deletes/create_form.ejs +1 -1
- package/src/views/articles/index.ejs +1 -1
- package/src/views/articles/translations/create_form.ejs +1 -1
- package/src/views/categories/_form.ejs +38 -0
- package/src/views/categories/create_form.ejs +9 -0
- package/src/views/categories/edit_form.ejs +9 -0
- package/src/views/categories/index.ejs +35 -0
- package/src/views/events/create_form.ejs +1 -1
- package/src/views/events/deletes/create_form.ejs +1 -1
- package/src/views/events/index.ejs +1 -1
- package/src/views/events/translations/create_form.ejs +1 -1
- package/src/views/layouts/application.ejs +1 -1
- package/src/views/pages/create_form.ejs +1 -1
- package/src/views/pages/deletes/create_form.ejs +1 -1
- package/src/views/pages/index.ejs +1 -1
- package/src/views/pages/translations/create_form.ejs +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import RaketaClient from "./RaketaClient";
|
|
2
|
+
import {
|
|
3
|
+
CategoriesDocument,
|
|
4
|
+
CategoryDocument,
|
|
5
|
+
CreateCategoryDocument,
|
|
6
|
+
UpdateCategoryDocument,
|
|
7
|
+
DeleteCategoryDocumet,
|
|
8
|
+
} from "./documents/categoryDocuments";
|
|
9
|
+
|
|
10
|
+
export default class Categories extends RaketaClient {
|
|
11
|
+
async list(query, locale, categoryType) {
|
|
12
|
+
const vars = this.buildListInput(query, locale);
|
|
13
|
+
|
|
14
|
+
const { categories: items } = await this.query(CategoriesDocument, {
|
|
15
|
+
input: {
|
|
16
|
+
...vars.input,
|
|
17
|
+
filters: { ...vars.input.filters, type: categoryType },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return items;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async all(query, locale, categoryType) {
|
|
25
|
+
const vars = this.buildAllInput(query, locale);
|
|
26
|
+
|
|
27
|
+
const { categories: items } = await this.query(CategoriesDocument, {
|
|
28
|
+
input: {
|
|
29
|
+
...vars.input,
|
|
30
|
+
filters: { ...vars.input.filters, type: categoryType },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return items;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async find(id, locale) {
|
|
38
|
+
const { category: item } = await this.query(CategoryDocument, {
|
|
39
|
+
input: {
|
|
40
|
+
id,
|
|
41
|
+
locale,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return item;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async create(params, locale, categoryType) {
|
|
49
|
+
const {
|
|
50
|
+
createCategory: { errors, tag },
|
|
51
|
+
} = await this.query(CreateCategoryDocument, {
|
|
52
|
+
input: {
|
|
53
|
+
locale,
|
|
54
|
+
category: {
|
|
55
|
+
type: categoryType,
|
|
56
|
+
slug: params.slug,
|
|
57
|
+
name: params.name,
|
|
58
|
+
settings: JSON.stringify(params.settings || {}),
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { tag, errors };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async update(id, params, locale, categoryType) {
|
|
67
|
+
const {
|
|
68
|
+
updateCategory: { errors, category },
|
|
69
|
+
} = await this.query(UpdateCategoryDocument, {
|
|
70
|
+
input: {
|
|
71
|
+
id,
|
|
72
|
+
locale,
|
|
73
|
+
category: {
|
|
74
|
+
type: categoryType,
|
|
75
|
+
slug: params.slug,
|
|
76
|
+
name: params.name,
|
|
77
|
+
settings: JSON.stringify(params.settings || {}),
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { category, errors };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async delete(id, locale) {
|
|
86
|
+
const {
|
|
87
|
+
deleteCategory: { errors, tag },
|
|
88
|
+
} = await this.query(DeleteCategoryDocumet, {
|
|
89
|
+
input: {
|
|
90
|
+
id,
|
|
91
|
+
locale,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { tag, errors };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -1,17 +1,5 @@
|
|
|
1
|
-
import { gql } from "
|
|
2
|
-
import { errorsFragment, paginationFragment } from "./common";
|
|
3
|
-
|
|
4
|
-
function categoryFragment() {
|
|
5
|
-
return gql`
|
|
6
|
-
{
|
|
7
|
-
id
|
|
8
|
-
type
|
|
9
|
-
slug
|
|
10
|
-
name
|
|
11
|
-
settings
|
|
12
|
-
}
|
|
13
|
-
`;
|
|
14
|
-
}
|
|
1
|
+
import { gql } from "graphql-request";
|
|
2
|
+
import { errorsFragment, paginationFragment, categoryFragment } from "./common";
|
|
15
3
|
|
|
16
4
|
const CategoriesDocument = gql`
|
|
17
5
|
query ($input: CategoriesInput!) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const index = async (ctx) => {
|
|
2
|
+
const items = await ctx.client.categories.list(
|
|
3
|
+
ctx.query,
|
|
4
|
+
ctx.locale,
|
|
5
|
+
ctx.resourceConfig.categoryType
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
assigns: {
|
|
10
|
+
items,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const create_form = async (ctx) => {
|
|
16
|
+
return {
|
|
17
|
+
assigns: {
|
|
18
|
+
resource: {},
|
|
19
|
+
errors: {},
|
|
20
|
+
_translations: [],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const create = async (ctx) => {
|
|
26
|
+
const params = { ...ctx.resourceParams.resource };
|
|
27
|
+
const resource = { ...params };
|
|
28
|
+
|
|
29
|
+
const { errors } = await ctx.client.categories.create(
|
|
30
|
+
resource,
|
|
31
|
+
ctx.locale,
|
|
32
|
+
ctx.resourceConfig.categoryType
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (errors) {
|
|
36
|
+
return {
|
|
37
|
+
assigns: {
|
|
38
|
+
resource,
|
|
39
|
+
|
|
40
|
+
errors: { resource: ctx.client.categories.formatErrors(errors) },
|
|
41
|
+
_translations: [],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
redirect: `/${ctx.locale}/${ctx.resourceConfig.categoryTypePlural}`,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const edit_form = async (ctx) => {
|
|
52
|
+
const { id } = ctx.router.req.param();
|
|
53
|
+
const resource = await ctx.client.categories.find(id, ctx.locale);
|
|
54
|
+
|
|
55
|
+
if (!resource) {
|
|
56
|
+
throw new Error(`Missing category #${id}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
assigns: {
|
|
61
|
+
resource,
|
|
62
|
+
errors: {},
|
|
63
|
+
_translations: [],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const update = async (ctx) => {
|
|
69
|
+
const { id } = ctx.params;
|
|
70
|
+
const params = ctx.resourceParams.resource;
|
|
71
|
+
|
|
72
|
+
const { errors } = await ctx.client.categories.update(
|
|
73
|
+
id,
|
|
74
|
+
{
|
|
75
|
+
name: params.name,
|
|
76
|
+
slug: params.slug,
|
|
77
|
+
settings: params.settings || {},
|
|
78
|
+
},
|
|
79
|
+
ctx.locale,
|
|
80
|
+
ctx.resourceConfig.categoryType
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (errors) {
|
|
84
|
+
return {
|
|
85
|
+
assigns: {
|
|
86
|
+
resource: { ...params, id },
|
|
87
|
+
errors: { resource: ctx.client.categories.formatErrors(errors) },
|
|
88
|
+
_translations: [],
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
redirect: `/${ctx.locale}/${ctx.resourceConfig.categoryTypePlural}`,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const destroy = async (ctx) => {
|
|
99
|
+
const { id } = ctx.params;
|
|
100
|
+
|
|
101
|
+
const { errors } = await ctx.client.categories.delete(id, ctx.locale);
|
|
102
|
+
|
|
103
|
+
if (errors) {
|
|
104
|
+
throw `Failed to delete category #${id}.`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
headers: {
|
|
109
|
+
"HX-redirect": `/${ctx.locale}/${ctx.resourceConfig.categoryTypePlural}`,
|
|
110
|
+
},
|
|
111
|
+
json: {},
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default {
|
|
116
|
+
index,
|
|
117
|
+
create_form,
|
|
118
|
+
create,
|
|
119
|
+
edit_form,
|
|
120
|
+
update,
|
|
121
|
+
destroy,
|
|
122
|
+
};
|
package/src/helpers/Inflector.js
CHANGED
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export { default as routeEventsModule } from "./routeEventsModule";
|
|
|
10
10
|
export { default as routeSettings } from "./routeSettings";
|
|
11
11
|
export { default as setupCloud } from "./setupCloud";
|
|
12
12
|
export { default as routeStandard } from "./routeStandard";
|
|
13
|
+
export { default as routeCategoriesModule } from "./routeCategoriesModule";
|
|
13
14
|
|
|
14
15
|
export { default as RaketaClient } from "./cloud/RaketaClient";
|
|
15
16
|
export { default as Redirects } from "./cloud/Redirects";
|
|
@@ -89,4 +90,3 @@ export { default as EventsHandler } from "./handlers/events/EventsHandler";
|
|
|
89
90
|
|
|
90
91
|
export { default as Factory } from "./tests/support/factory";
|
|
91
92
|
export { default as Logger } from "./Logger";
|
|
92
|
-
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import routeCollection from "./routeCollection";
|
|
2
|
+
import CategoriesHandler from "./handlers/categories";
|
|
3
|
+
|
|
4
|
+
function routeCategoriesModule(adapter, categoryType, categoryTypePlural) {
|
|
5
|
+
const resourceConfig = {
|
|
6
|
+
categoryType,
|
|
7
|
+
categoryTypePlural,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
routeCollection(adapter, categoryTypePlural, CategoriesHandler, {
|
|
11
|
+
...resourceConfig,
|
|
12
|
+
viewsFallback: "categories",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default routeCategoriesModule;
|
package/src/setupCloud.js
CHANGED
|
@@ -10,6 +10,7 @@ import ArticleChanges from "./cloud/ArticleChanges";
|
|
|
10
10
|
import Events from "./cloud/Events";
|
|
11
11
|
import EventRevisions from "./cloud/EventRevisions";
|
|
12
12
|
import EventChanges from "./cloud/EventChanges";
|
|
13
|
+
import Categories from "./cloud/Categories";
|
|
13
14
|
|
|
14
15
|
function setupCloud(ctx, getConfig) {
|
|
15
16
|
if (!ctx?.authentication?.token) return;
|
|
@@ -43,6 +44,7 @@ function setupCloud(ctx, getConfig) {
|
|
|
43
44
|
getConfig,
|
|
44
45
|
ctx.authentication.token
|
|
45
46
|
);
|
|
47
|
+
ctx.client.categories = new Categories(getConfig, ctx.authentication.token);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export default setupCloud;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import CategoriesHandler from "../../../handlers/categories";
|
|
2
|
+
import Categories from "../../../cloud/Categories";
|
|
3
|
+
|
|
4
|
+
import Factory from "../../support/factory";
|
|
5
|
+
|
|
6
|
+
function buildCtx(category) {
|
|
7
|
+
return {
|
|
8
|
+
client: {
|
|
9
|
+
categories: new Categories(() => ({}), "token"),
|
|
10
|
+
},
|
|
11
|
+
resourceParams: {
|
|
12
|
+
resource: {
|
|
13
|
+
slug: category.slug,
|
|
14
|
+
name: category.name,
|
|
15
|
+
settings: { ...category.settings },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
locale: "en",
|
|
19
|
+
resourceConfig: {
|
|
20
|
+
categoryType: "cat",
|
|
21
|
+
categoryTypePlural: "cats",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("Categories create", () => {
|
|
27
|
+
test("create successful", async () => {
|
|
28
|
+
const category = Factory.build("category", { settings: { foo: "bar" } });
|
|
29
|
+
const ctx = buildCtx(category);
|
|
30
|
+
|
|
31
|
+
const mockCreate = jest
|
|
32
|
+
.spyOn(ctx.client.categories, "create")
|
|
33
|
+
.mockImplementation(() => ({ category: category, errors: null }));
|
|
34
|
+
|
|
35
|
+
const result = await CategoriesHandler.create(ctx);
|
|
36
|
+
|
|
37
|
+
expect(mockCreate).toHaveBeenCalledWith(
|
|
38
|
+
{
|
|
39
|
+
slug: category.slug,
|
|
40
|
+
name: category.name,
|
|
41
|
+
settings: category.settings,
|
|
42
|
+
},
|
|
43
|
+
"en",
|
|
44
|
+
"cat"
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual({
|
|
48
|
+
redirect: "/en/cats",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("create failed", async () => {
|
|
53
|
+
const category = Factory.build("category", {
|
|
54
|
+
slug: "",
|
|
55
|
+
settings: { foo: "bar" },
|
|
56
|
+
});
|
|
57
|
+
const ctx = buildCtx(category);
|
|
58
|
+
|
|
59
|
+
const mockCreate = jest
|
|
60
|
+
.spyOn(ctx.client.categories, "create")
|
|
61
|
+
.mockImplementation(() => ({
|
|
62
|
+
categories: null,
|
|
63
|
+
errors: [{ key: "url", message: "already used" }],
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
const result = await CategoriesHandler.create(ctx);
|
|
67
|
+
|
|
68
|
+
expect(mockCreate).toHaveBeenCalledWith(
|
|
69
|
+
{
|
|
70
|
+
slug: "",
|
|
71
|
+
name: category.name,
|
|
72
|
+
settings: category.settings,
|
|
73
|
+
},
|
|
74
|
+
"en",
|
|
75
|
+
"cat"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({
|
|
79
|
+
assigns: {
|
|
80
|
+
resource: {
|
|
81
|
+
slug: "",
|
|
82
|
+
name: category.name,
|
|
83
|
+
settings: category.settings,
|
|
84
|
+
},
|
|
85
|
+
errors: {
|
|
86
|
+
resource: {
|
|
87
|
+
url: "already used",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
_translations: [],
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import CategoriesHandler from "../../../handlers/categories";
|
|
2
|
+
import Categories from "../../../cloud/Categories";
|
|
3
|
+
|
|
4
|
+
function buildCtx() {
|
|
5
|
+
return {
|
|
6
|
+
client: {
|
|
7
|
+
categories: new Categories(() => ({}), "token"),
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("Categories new", () => {
|
|
13
|
+
test("create_form", async () => {
|
|
14
|
+
const ctx = buildCtx();
|
|
15
|
+
|
|
16
|
+
const result = await CategoriesHandler.create_form(ctx);
|
|
17
|
+
|
|
18
|
+
expect(result).toEqual({
|
|
19
|
+
assigns: {
|
|
20
|
+
resource: {},
|
|
21
|
+
errors: {},
|
|
22
|
+
_translations: [],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import RedirectsHandler from "../../../handlers/RedirectsHandler";
|
|
2
|
+
import Redirects from "../../../cloud/Redirects";
|
|
3
|
+
|
|
4
|
+
import Factory from "../../support/factory";
|
|
5
|
+
|
|
6
|
+
function buildCtx() {
|
|
7
|
+
return {
|
|
8
|
+
client: {
|
|
9
|
+
redirects: new Redirects(() => ({}), "token"),
|
|
10
|
+
},
|
|
11
|
+
params: { id: 1 },
|
|
12
|
+
locale: "en",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("Redirects", () => {
|
|
17
|
+
test("destroy successful", async () => {
|
|
18
|
+
const redirect = Factory.build("redirect");
|
|
19
|
+
const ctx = buildCtx();
|
|
20
|
+
|
|
21
|
+
const mockDelete = jest
|
|
22
|
+
.spyOn(ctx.client.redirects, "delete")
|
|
23
|
+
.mockImplementation(() => ({ redirect: redirect, errors: null }));
|
|
24
|
+
|
|
25
|
+
const result = await RedirectsHandler.destroy(ctx);
|
|
26
|
+
|
|
27
|
+
expect(mockDelete).toHaveBeenCalledWith(1);
|
|
28
|
+
|
|
29
|
+
expect(result).toEqual({
|
|
30
|
+
headers: {
|
|
31
|
+
"HX-Redirect": "/en/redirects",
|
|
32
|
+
},
|
|
33
|
+
json: {},
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("destroy failed", async () => {
|
|
38
|
+
const ctx = buildCtx();
|
|
39
|
+
|
|
40
|
+
const mockDelete = jest
|
|
41
|
+
.spyOn(ctx.client.redirects, "delete")
|
|
42
|
+
.mockImplementation(() => ({
|
|
43
|
+
redirect: null,
|
|
44
|
+
errors: [{ key: "generalError", message: "Not found" }],
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
await expect(() => RedirectsHandler.destroy(ctx)).rejects.toMatch(
|
|
48
|
+
"Failed to delete redirect #1. WTF?"
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(mockDelete).toHaveBeenCalledWith(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import CategoriesHandler from "../../../handlers/categories";
|
|
2
|
+
import Categories from "../../../cloud/Categories";
|
|
3
|
+
|
|
4
|
+
import Factory from "../../support/factory";
|
|
5
|
+
|
|
6
|
+
function buildCtx() {
|
|
7
|
+
return {
|
|
8
|
+
client: {
|
|
9
|
+
categories: new Categories(() => ({}), "token"),
|
|
10
|
+
},
|
|
11
|
+
router: {
|
|
12
|
+
req: {
|
|
13
|
+
param: () => ({ id: 1 }),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
resourceConfig: {
|
|
17
|
+
categoryType: "cat",
|
|
18
|
+
categoryTypePlural: "cats",
|
|
19
|
+
},
|
|
20
|
+
locale: "en",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("Categories edit", () => {
|
|
25
|
+
test("edit_form", async () => {
|
|
26
|
+
const category = Factory.build("category");
|
|
27
|
+
const ctx = buildCtx();
|
|
28
|
+
|
|
29
|
+
const mockFind = jest
|
|
30
|
+
.spyOn(ctx.client.categories, "find")
|
|
31
|
+
.mockImplementation(() => category);
|
|
32
|
+
|
|
33
|
+
const result = await CategoriesHandler.edit_form(ctx);
|
|
34
|
+
|
|
35
|
+
expect(mockFind).toHaveBeenCalledWith(1, "en");
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
assigns: {
|
|
39
|
+
resource: { ...category },
|
|
40
|
+
errors: {},
|
|
41
|
+
_translations: [],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("edit_form when a category does not exist", async () => {
|
|
47
|
+
const ctx = buildCtx();
|
|
48
|
+
|
|
49
|
+
const mockFind = jest
|
|
50
|
+
.spyOn(ctx.client.categories, "find")
|
|
51
|
+
.mockImplementation(() => null);
|
|
52
|
+
|
|
53
|
+
expect(async () => {
|
|
54
|
+
await CategoriesHandler.edit_form(ctx);
|
|
55
|
+
}).rejects.toThrow("Missing category #1");
|
|
56
|
+
|
|
57
|
+
expect(mockFind).toHaveBeenCalledWith(1, "en");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import CategoriesHandler from "../../../handlers/categories";
|
|
2
|
+
import Categories from "../../../cloud/Categories";
|
|
3
|
+
|
|
4
|
+
import Factory from "../../support/factory";
|
|
5
|
+
|
|
6
|
+
describe("Categories list", () => {
|
|
7
|
+
function buildCtx() {
|
|
8
|
+
return {
|
|
9
|
+
client: {
|
|
10
|
+
categories: new Categories(() => ({}), "token"),
|
|
11
|
+
},
|
|
12
|
+
query: {},
|
|
13
|
+
req: {
|
|
14
|
+
path: "/cats",
|
|
15
|
+
},
|
|
16
|
+
resourceConfig: {
|
|
17
|
+
categoryType: "cat",
|
|
18
|
+
categoryTypePlural: "cats",
|
|
19
|
+
},
|
|
20
|
+
locale: "en",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("index", async () => {
|
|
25
|
+
const category = Factory.build("category");
|
|
26
|
+
const pagination = Factory.build("pagination");
|
|
27
|
+
const ctx = buildCtx();
|
|
28
|
+
|
|
29
|
+
const mockedList = jest
|
|
30
|
+
.spyOn(ctx.client.categories, "list")
|
|
31
|
+
.mockImplementation(() => ({ list: [category], pagination }));
|
|
32
|
+
|
|
33
|
+
const result = await CategoriesHandler.index(ctx);
|
|
34
|
+
|
|
35
|
+
expect(mockedList).toHaveBeenCalledWith({}, "en", "cat");
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
assigns: {
|
|
39
|
+
items: {
|
|
40
|
+
list: [category],
|
|
41
|
+
pagination: pagination,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import CategoriesHandler from "../../../handlers/categories";
|
|
2
|
+
import Categories from "../../../cloud/Categories";
|
|
3
|
+
|
|
4
|
+
function buildCtx(id, updateParams) {
|
|
5
|
+
return {
|
|
6
|
+
client: {
|
|
7
|
+
categories: new Categories(() => ({}), "token"),
|
|
8
|
+
},
|
|
9
|
+
params: { id },
|
|
10
|
+
resourceParams: {
|
|
11
|
+
resource: {
|
|
12
|
+
slug: updateParams.slug,
|
|
13
|
+
name: updateParams.name,
|
|
14
|
+
settings: updateParams.settings,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
locale: "en",
|
|
18
|
+
resourceConfig: {
|
|
19
|
+
categoryType: "cat",
|
|
20
|
+
categoryTypePlural: "cats",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("Categories update", () => {
|
|
26
|
+
test("update successful", async () => {
|
|
27
|
+
const updateParams = {
|
|
28
|
+
name: "Updated cateogry",
|
|
29
|
+
slug: "updated",
|
|
30
|
+
settings: {
|
|
31
|
+
foo: "bar",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const ctx = buildCtx(1, updateParams);
|
|
35
|
+
|
|
36
|
+
const mockUpdate = jest
|
|
37
|
+
.spyOn(ctx.client.categories, "update")
|
|
38
|
+
.mockImplementation(() => ({
|
|
39
|
+
category: { ...updateParams, id: 1 },
|
|
40
|
+
errors: null,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const result = await CategoriesHandler.update(ctx);
|
|
44
|
+
|
|
45
|
+
expect(mockUpdate).toHaveBeenCalledWith(1, updateParams, "en", "cat");
|
|
46
|
+
|
|
47
|
+
expect(result).toEqual({
|
|
48
|
+
redirect: "/en/cats",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("update failed", async () => {
|
|
53
|
+
const updateParams = {
|
|
54
|
+
name: "Updated cateogry",
|
|
55
|
+
slug: "updated",
|
|
56
|
+
settings: {
|
|
57
|
+
foo: "bar",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const ctx = buildCtx(1, updateParams);
|
|
61
|
+
|
|
62
|
+
const mockUpdate = jest
|
|
63
|
+
.spyOn(ctx.client.categories, "update")
|
|
64
|
+
.mockImplementation(() => ({
|
|
65
|
+
category: null,
|
|
66
|
+
errors: [{ key: "slug", message: "already used" }],
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const result = await CategoriesHandler.update(ctx);
|
|
70
|
+
|
|
71
|
+
expect(mockUpdate).toHaveBeenCalledWith(1, updateParams, "en", "cat");
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
assigns: {
|
|
75
|
+
resource: {
|
|
76
|
+
id: 1,
|
|
77
|
+
...updateParams,
|
|
78
|
+
},
|
|
79
|
+
errors: {
|
|
80
|
+
resource: {
|
|
81
|
+
slug: "already used",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
_translations: [],
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -122,6 +122,13 @@ const Tag = {
|
|
|
122
122
|
name: "Tag",
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
+
const CATEGORY = {
|
|
126
|
+
id: 42,
|
|
127
|
+
type: "entertainment",
|
|
128
|
+
name: "Movie",
|
|
129
|
+
slug: "movie",
|
|
130
|
+
};
|
|
131
|
+
|
|
125
132
|
const FIXTURES = {
|
|
126
133
|
redirect: {
|
|
127
134
|
id: "1",
|
|
@@ -144,6 +151,7 @@ const FIXTURES = {
|
|
|
144
151
|
event_change: { ...EVENT_CHANGE },
|
|
145
152
|
settings: { value: {} },
|
|
146
153
|
tag: Tag,
|
|
154
|
+
category: { ...CATEGORY },
|
|
147
155
|
pagination: {
|
|
148
156
|
currentPage: 1,
|
|
149
157
|
pagesCount: 1,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Create <%= Inflector.downcase(_context.resourceConfig.articleType) %></h1>
|
|
3
|
+
<h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.articleType)) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Delete <%= Inflector.downcase(_context.resourceConfig.articleType) %></h1>
|
|
3
|
+
<h1 class="heading-1">Delete <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.articleType)) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1"><%= Inflector.capitalize(_context.resourceConfig.articleTypePlural) %></h1>
|
|
3
|
+
<h1 class="heading-1"><%= Inflector.humanize(Inflector.capitalize(_context.resourceConfig.articleTypePlural)) %></h1>
|
|
4
4
|
|
|
5
5
|
<div class="actions-toolbar">
|
|
6
6
|
<a href="<%= `/${_context.locale}/${_context.resourceConfig.articleTypePlural}/new` %>" class="btn-primary">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Create <%= Inflector.downcase(_context.resourceConfig.articleType) %> translation</h1>
|
|
3
|
+
<h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.articleType)) %> translation</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<% form_for({ resource }, errors, (f) => { %>
|
|
2
|
+
<form action="<%= resource?.id ? `/${_context.locale}/${_context.resourceConfig.categoryTypePlural}/${resource.id}` : `/${_context.locale}/${_context.resourceConfig.categoryTypePlural}` %>" method="post" class="form">
|
|
3
|
+
<%- f.field('resource.name', { required: true }) %>
|
|
4
|
+
<%- f.field('resource.slug', { label: "URL", required: true }) %>
|
|
5
|
+
|
|
6
|
+
<div class="form-actions">
|
|
7
|
+
<div class="btn-toolbar">
|
|
8
|
+
<%- f.submit('Save record', { class: 'btn-primary' }) %>
|
|
9
|
+
|
|
10
|
+
<a href="<%= `/${_context.locale}/${_context.resourceConfig.categoryTypePlural}` %>" class="btn-secondary">Back to list</a>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<% if (resource?.id) {%>
|
|
14
|
+
<button hx-delete="<%= `/${_context.locale}/${_context.resourceConfig.categoryTypePlural}/${resource.id}` %>" hx-confirm="Are you sure?" class="btn-danger has-icon">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
|
17
|
+
</svg>
|
|
18
|
+
|
|
19
|
+
Delete record
|
|
20
|
+
</button>
|
|
21
|
+
<% } %>
|
|
22
|
+
</div>
|
|
23
|
+
</form>
|
|
24
|
+
<% }) %>
|
|
25
|
+
|
|
26
|
+
<div>
|
|
27
|
+
<div class="help">
|
|
28
|
+
<div class="flex items-center gap-1 mb-2">
|
|
29
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
|
30
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
|
|
31
|
+
</svg>
|
|
32
|
+
|
|
33
|
+
<h3 class="heading-3">Contextual help</h3>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore nostrum fugit sint sunt distinctio ut cum, corrupti ad error, quo labore minima. Reprehenderit quasi maxime repudiandae voluptatum commodi saepe molestiae?</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="page-wrapper">
|
|
2
|
+
<div class="page-title">
|
|
3
|
+
<h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.categoryType)) %></h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="grid gap-4 md:grid-cols-3">
|
|
7
|
+
<%- render_partial('_form.ejs') %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<div class="page-wrapper">
|
|
2
|
+
<div class="page-title">
|
|
3
|
+
<h1 class="heading-1">Edit <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.categoryType)) %></h1>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="grid gap-4 md:grid-cols-3">
|
|
7
|
+
<%- render_partial('_form.ejs') %>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<div class="page-wrapper">
|
|
2
|
+
<div class="page-title">
|
|
3
|
+
<h1 class="heading-1"><%= Inflector.humanize(Inflector.capitalize(_context.resourceConfig.categoryTypePlural)) %></h1>
|
|
4
|
+
|
|
5
|
+
<div class="actions-toolbar">
|
|
6
|
+
<a href="<%= `/${_context.locale}/${_context.resourceConfig.categoryTypePlural}/new` %>" class="btn-primary">
|
|
7
|
+
Create new
|
|
8
|
+
</a>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<table class="data-grid">
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th><%- sort_button("Name", "name", _context) %></th>
|
|
16
|
+
<th><%- sort_button("URL", "slug", _context) %></th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
|
|
20
|
+
<tbody>
|
|
21
|
+
<% items.list.forEach((item, idx)=> { %>
|
|
22
|
+
<tr>
|
|
23
|
+
<td class="w-1/2">
|
|
24
|
+
<a href="<%= `/${_context.locale}/${_context.resourceConfig.categoryTypePlural}/${item.id}/edit` %>" class="btn-text mr-1">
|
|
25
|
+
<%= item.name %>
|
|
26
|
+
</a>
|
|
27
|
+
</td>
|
|
28
|
+
<td><%= item.slug %></td>
|
|
29
|
+
</tr>
|
|
30
|
+
<% }); %>
|
|
31
|
+
</tbody>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
34
|
+
<%- pagination(items.pagination, _context) %>
|
|
35
|
+
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Create <%= Inflector.downcase(_context.resourceConfig.eventType) %></h1>
|
|
3
|
+
<h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.eventType)) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Delete <%= Inflector.downcase(_context.resourceConfig.eventType) %></h1>
|
|
3
|
+
<h1 class="heading-1">Delete <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.eventType)) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1"><%= Inflector.capitalize(_context.resourceConfig.eventTypePlural) %></h1>
|
|
3
|
+
<h1 class="heading-1"><%= Inflector.humanize(Inflector.capitalize(_context.resourceConfig.eventTypePlural)) %></h1>
|
|
4
4
|
|
|
5
5
|
<div class="actions-toolbar">
|
|
6
6
|
<a href="<%= `/${_context.locale}/${_context.resourceConfig.eventTypePlural}/new` %>" class="btn-primary">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Create <%= Inflector.downcase(_context.resourceConfig.eventType) %> translation</h1>
|
|
3
|
+
<h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.eventType)) %> translation</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
<div class="pl-12 py-4 bg-raketa-background/50 group-[.expanded]:block hidden">
|
|
49
49
|
<% menu.submenus.forEach(submenu => { %>
|
|
50
|
-
<a href="<%= `/${_context.locale}
|
|
50
|
+
<a href="<%= `/${_context.locale}${submenu.link}` %>" class="block py-2 body-2 text-raketa-foreground/50 hover:text-raketa-foreground transition-all ease-in-out duration-300"><%= submenu.label %></a>
|
|
51
51
|
<% }) %>
|
|
52
52
|
</div>
|
|
53
53
|
</details>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Create <%= Inflector.downcase(_context.resourceConfig.pageType) %></h1>
|
|
3
|
+
<h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.pageType)) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Delete <%= Inflector.downcase(_context.resourceConfig.pageType) %></h1>
|
|
3
|
+
<h1 class="heading-1">Delete <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.pageType)) %></h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1"><%= Inflector.capitalize(_context.resourceConfig.pageTypePlural) %></h1>
|
|
3
|
+
<h1 class="heading-1"><%= Inflector.humanize(Inflector.capitalize(_context.resourceConfig.pageTypePlural)) %></h1>
|
|
4
4
|
|
|
5
5
|
<div class="actions-toolbar">
|
|
6
6
|
<a href="<%= `/${_context.locale}/${_context.resourceConfig.pageTypePlural}/new` %>" class="btn-primary">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="page-wrapper">
|
|
2
2
|
<div class="page-title">
|
|
3
|
-
<h1 class="heading-1">Create <%= Inflector.downcase(_context.resourceConfig.pageType) %> translation</h1>
|
|
3
|
+
<h1 class="heading-1">Create <%= Inflector.humanize(Inflector.downcase(_context.resourceConfig.pageType)) %> translation</h1>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
6
|
<div class="grid gap-4 md:grid-cols-3">
|