druxt 0.15.0 → 0.18.0

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.
@@ -0,0 +1,48 @@
1
+ import DruxtDebug from './DruxtDebug.vue'
2
+
3
+ export default {
4
+ title: 'Druxt/DruxtDebug',
5
+ component: DruxtDebug
6
+ }
7
+
8
+ const Template = (args) => ({
9
+ components: { DruxtDebug },
10
+ props: Object.keys(args),
11
+ template: `<DruxtDebug v-bind="$props">Debug information.</DruxtDebug>`
12
+ })
13
+
14
+ export const Default = Template.bind({})
15
+ Default.storyName = 'Default slot'
16
+
17
+ export const Json = Template.bind({})
18
+ Json.args = {
19
+ json: {
20
+ type: '',
21
+ id: '',
22
+ links: {},
23
+ attributes: {},
24
+ relationships: {},
25
+ },
26
+ open: true,
27
+ }
28
+ Json.parameters = {
29
+ docs: {
30
+ storyDescription: 'The **json** prop takes any data and runs it through a basic JSON formatter for improved readability.',
31
+ source: {
32
+ code: '<DruxtDebug :json="entity" open>Debug information</DruxtDebug>'
33
+ },
34
+ },
35
+ }
36
+
37
+ export const Summary = Template.bind({})
38
+ Summary.args = {
39
+ summary: 'A brief debug message'
40
+ }
41
+ Summary.parameters = {
42
+ docs: {
43
+ storyDescription: 'The **summary** prop can be used to provide a quick explanation of the debug component.',
44
+ source: {
45
+ code: `<DruxtDebug summary="${Summary.args.sumary}">Debug information</DruxtDebug>`
46
+ },
47
+ },
48
+ }
@@ -4,6 +4,7 @@
4
4
  <pre v-if="json"><!--
5
5
  --><code>{{ JSON.stringify(json, null, ' ') }}</code><!--
6
6
  --></pre>
7
+ <!-- @slot Debug content -->
7
8
  <slot />
8
9
  </details>
9
10
 
@@ -13,10 +14,14 @@
13
14
  <script>
14
15
 
15
16
  /**
16
- * Renders Debug data only when Nuxt is in development mode.
17
+ * The DruxtDebug component can be used to render debug information that is
18
+ * visible only when Nuxt is run in development mode.
19
+ *
20
+ * You can render your debug information through the default slot, or via the
21
+ * **json** prop to format your data, or a combination of the two.
17
22
  *
18
23
  * @example @lang vue
19
- * <DruxtDebug :json="entity" />
24
+ * <DruxtDebug :json="entity">Debug information</DruxtDebug>
20
25
  */
21
26
  export default {
22
27
  /** */
@@ -0,0 +1,126 @@
1
+ import { Canvas, Meta, Story } from '@storybook/addon-docs'
2
+
3
+ <Meta title="Druxt/DruxtModule" />
4
+
5
+ # DruxtModule
6
+
7
+ The DruxtModule component is the core component to be extended by core and
8
+ custom modules.
9
+
10
+ ```js
11
+ import DruxtModule from 'druxt/dist/components/DruxtModule.vue'
12
+ export default {
13
+ extends: DruxtModule
14
+ }
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Fetch config
20
+
21
+ All modules have a two part fetch system available to them; **fetchConfig** and
22
+ **fetchData**.
23
+
24
+ ```js
25
+ async fetchConfig() {
26
+ // Load configuration data here.
27
+ this.config = await this.getResource({...})
28
+ },
29
+ ```
30
+
31
+ Fetch config is used to load information from Drupal to determine the default
32
+ behaviour of that component, as well as to determine the available component
33
+
34
+ ---
35
+
36
+ ## Component options
37
+
38
+ The Druxt Wrapper system looks for available Vue components using a specific
39
+ naming convention, determined by the Druxt module **componentOptions**.
40
+
41
+ This allows for targetted theming of components based on the props or
42
+ configuration data.
43
+
44
+ ```js
45
+ componentOptions: (context) => ([[context.attributes.foo, 'default']]),
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Fetch data
51
+
52
+ After the Component options are determined, the first available component is
53
+ determined, and any exported Druxt settings are loaded and used to fetch the
54
+ main module data.
55
+
56
+ ```js
57
+ async fetchData(settings) {
58
+ // Load data using the settings determine based on the configuration data.
59
+ this.resource = await this.getResource({
60
+ type: this.config.attributes.foo,
61
+ id: this.id,
62
+ query: this.getQuery(settings)
63
+ })
64
+ },
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Props data
70
+
71
+ The **propsData** callback returns the props data for the render Wrapper
72
+ component, making props data available to the frontend developer.
73
+
74
+ ```js
75
+ propsData: (context) => ({ foo: context.config.attributes.foo })
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Slots
81
+
82
+ The **slots** method builds the slots that are made available to the Wrapper
83
+ component. It is expected to return an object of scoped slot functions.
84
+
85
+ ```
86
+ slots(h) {
87
+ return {
88
+ default: () => h('div', ['Hello world']),
89
+ }
90
+ }
91
+ ```
92
+
93
+ ---
94
+
95
+ ### Custom module boilerplate
96
+
97
+ Paste this into a Vue component as a starting point for a custom or contrib
98
+ Druxt module.
99
+
100
+ ```vue
101
+ <script>
102
+ import DruxtModule from 'druxt/dist/components/DruxtModule.vue'
103
+ export default {
104
+ name: 'MyModule',
105
+ extends: DruxtModule
106
+ druxt: {
107
+ async fetchConfig() {
108
+ // Load configuration data here.
109
+ },
110
+ async fetchData(settings) {
111
+ // Load data using the settings determine based on the configuration data.
112
+ },
113
+ // Build nested arrays of component options based on the context.
114
+ componentOptions: (context) => ([['default']]),
115
+ // Map props data from the context.
116
+ propsData: (context) => ({ ...context }),
117
+ // Build slots render arrays.
118
+ slots(h) {
119
+ return {
120
+ default: () => h('div', ['Hello world'])
121
+ }
122
+ }
123
+ }
124
+ }
125
+ </script>
126
+ ```
@@ -6,7 +6,11 @@ import Vue from 'vue'
6
6
  import DruxtWrapper from './DruxtWrapper.vue'
7
7
 
8
8
  /**
9
- * Base component for core and custom Druxt modules.
9
+ * The DruxtModule component is used to make a Druxt module, simply import and
10
+ * extend the component to get started.
11
+ *
12
+ * The component provides access to the Druxt Wrapper theming and fetch system
13
+ * via the druxt settings object.
10
14
  *
11
15
  * @example @lang js
12
16
  * import DruxtModule from 'druxt/dist/components/DruxtModule.vue'
@@ -14,11 +18,18 @@ import DruxtWrapper from './DruxtWrapper.vue'
14
18
  * name: 'DruxtTestModule',
15
19
  * extends: DruxtModule,
16
20
  * druxt: {
17
- * componentOptions: () => ([['wrapper']]),
18
- * propsData: (ctx) => ({
19
- * bar: ctx.bar,
20
- * foo: ctx.foo,
21
+ * async fetchConfig() {},
22
+ * async fetchData(settings) {},
23
+ * componentOptions: (context) => ([[context.foo, context.bar, 'default']]),
24
+ * propsData: (context) => ({
25
+ * foo: context.foo,
26
+ * bar: context.bar,
21
27
  * }),
28
+ * slots(h) {
29
+ * return {
30
+ * default: (attrs) => h('DruxtDebug', ['Hello world'])
31
+ * }
32
+ * }
22
33
  * }
23
34
  * }
24
35
  */
@@ -107,12 +118,16 @@ export default {
107
118
 
108
119
  // Fetch configuration.
109
120
  if ((this.$options.druxt || {}).fetchConfig) {
110
- await this.$options.druxt.fetchConfig.call(this)
121
+ try {
122
+ await this.$options.druxt.fetchConfig.call(this)
123
+ } catch(err) {
124
+ return this.error(err)
125
+ }
111
126
  }
112
127
 
113
128
  // Build wrapper component object.
114
129
  let options = []
115
- const hasDefaultTemplate = !!(this.$vnode.data.scopedSlots || {}).default
130
+ const hasDefaultTemplate = !!(((this.$vnode || {}).data || {}).scopedSlots || {}).default
116
131
  // Load wrapper components if:
117
132
  if (
118
133
  // No default template and wrapper isn't false OR
@@ -138,7 +153,11 @@ export default {
138
153
 
139
154
  // Fetch resource.
140
155
  if ((this.$options.druxt || {}).fetchData) {
141
- await this.$options.druxt.fetchData.call(this, component.settings)
156
+ try {
157
+ await this.$options.druxt.fetchData.call(this, component.settings)
158
+ } catch(err) {
159
+ return this.error(err, { component })
160
+ }
142
161
  }
143
162
 
144
163
  // Get scoped slots.
@@ -153,7 +172,7 @@ export default {
153
172
 
154
173
  watch: {
155
174
  model() {
156
- if (this.component.props.value !== this.model) {
175
+ if (this.component.props && this.component.props.value !== this.model) {
157
176
  this.component.props.value = this.model
158
177
 
159
178
  // Only emit 'input' if using the default 'DruxtWrapper' component.
@@ -172,6 +191,31 @@ export default {
172
191
 
173
192
  /** */
174
193
  methods: {
194
+ /**
195
+ * Sets the component to render a DruxtDebug error message.
196
+ */
197
+ error(err, context = {}) {
198
+ // Build error details.
199
+ const { url } = err.druxt || {}
200
+ const title = (err.response || {}).statusText || ((((err.response || {}).data || {}).errors || [])[0] || {}).title
201
+ const summary = (err.response || {}).status
202
+ ? [(err.response || {}).status, title].filter((s) => s).join(': ')
203
+ : err.message
204
+
205
+ // Set the component to a Debug component with error details.
206
+ this.component = {
207
+ ...context.component || {},
208
+ is: 'DruxtDebug',
209
+ props: {
210
+ json: {
211
+ url,
212
+ errors: ((err.response || {}).data || {}).errors
213
+ },
214
+ summary
215
+ }
216
+ }
217
+ },
218
+
175
219
  /**
176
220
  * Get list of module wrapper components.
177
221
  *
@@ -193,7 +237,7 @@ export default {
193
237
  let components = []
194
238
  for (const set of options.filter(set => Array.isArray(set))) {
195
239
  const variants = []
196
- components = [...components, ...set.map(item => {
240
+ components = [...components, ...set.filter((o) => o).map(item => {
197
241
  // Build array of name parts.
198
242
  const parts = variants.length ? [...variants[0].parts] : []
199
243
  parts.push(pascalCase(splitByCase(item)))
@@ -265,6 +309,18 @@ export default {
265
309
  */
266
310
  getScopedSlots() {
267
311
  const h = this.$createElement
312
+
313
+ // Get an array of unmet required props.
314
+ const required = Object.entries(this.$options.props)
315
+ .filter(([k, o]) => o.required && !this[k])
316
+ .map(([k]) => k)
317
+ // Return debug error if any missing props are found.
318
+ if (required.length) {
319
+ return {
320
+ default: () => h('DruxtDebug', { props: { summary: `Missing required props: ${required.join(', ')}.` } })
321
+ }
322
+ }
323
+
268
324
  const scopedSlots = typeof (this.$options.druxt || {}).slots === 'function'
269
325
  ? this.$options.druxt.slots.call(this, h)
270
326
  : {}
package/dist/druxt.esm.js CHANGED
@@ -9,7 +9,7 @@ import Vue from 'vue';
9
9
  import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
10
10
 
11
11
  var name = "druxt";
12
- var version = "0.15.0";
12
+ var version = "0.18.0";
13
13
  var description = "The Fully Decoupled Drupal Framework for Nuxt.js.";
14
14
  var keywords = [
15
15
  "cms",
@@ -43,11 +43,12 @@ var main = "dist/druxt.ssr.js";
43
43
  var module = "dist/druxt.esm.js";
44
44
  var files = [
45
45
  "dist",
46
+ "stories",
46
47
  "templates"
47
48
  ];
48
49
  var dependencies = {
50
+ "@nuxtjs/axios": "^5.13.6",
49
51
  "@nuxtjs/proxy": "^2.1.0",
50
- axios: "^0.21.1",
51
52
  chalk: "^4.1.2",
52
53
  deepmerge: "^4.2.2",
53
54
  "drupal-jsonapi-params": "^1.2.1",
@@ -55,7 +56,11 @@ var dependencies = {
55
56
  querystring: "^0.2.0",
56
57
  scule: "^0.2.0"
57
58
  };
59
+ var devDependencies = {
60
+ "@storybook/addon-docs": "^6.4.13"
61
+ };
58
62
  var peerDependencies = {
63
+ axios: "*",
59
64
  consola: "*"
60
65
  };
61
66
  var optionalDependencies = {
@@ -80,6 +85,7 @@ var meta = {
80
85
  module: module,
81
86
  files: files,
82
87
  dependencies: dependencies,
88
+ devDependencies: devDependencies,
83
89
  peerDependencies: peerDependencies,
84
90
  optionalDependencies: optionalDependencies,
85
91
  publishConfig: publishConfig
@@ -119,6 +125,14 @@ const DruxtNuxtModule = function(moduleOptions = {}) {
119
125
  }
120
126
  this.addModule("@nuxtjs/proxy");
121
127
  }
128
+ if (!options.axios) {
129
+ this.options.axios = {
130
+ baseURL: options.baseUrl,
131
+ proxy: !!this.options.proxy,
132
+ ...this.options.axios
133
+ };
134
+ this.addModule("@nuxtjs/axios");
135
+ }
122
136
  this.nuxt.hook("components:dirs", (dirs) => {
123
137
  dirs.push({ path: join(__dirname, "components") });
124
138
  });
@@ -127,6 +141,18 @@ const DruxtNuxtModule = function(moduleOptions = {}) {
127
141
  fileName: "druxt.js",
128
142
  options
129
143
  });
144
+ const extendPlugins = this.options.extendPlugins;
145
+ this.options.extendPlugins = (plugins) => {
146
+ plugins = typeof extendPlugins === "function" ? extendPlugins(plugins) : plugins;
147
+ const axiosIndex = plugins.findIndex(({ src }) => src === `${this.options.buildDir}/axios.js`);
148
+ const axiosPlugin = plugins[axiosIndex];
149
+ plugins.splice(axiosIndex, 1);
150
+ const druxtIndex = plugins.findIndex(({ src }) => src === `${this.options.buildDir}/druxt.js`);
151
+ const druxtPlugin = plugins[druxtIndex];
152
+ plugins.splice(druxtIndex, 1);
153
+ plugins = [axiosPlugin, druxtPlugin, ...plugins];
154
+ return plugins;
155
+ };
130
156
  this.addPlugin({
131
157
  src: resolve(__dirname, "../templates/store.js"),
132
158
  fileName: "store/druxt.js",
@@ -136,8 +162,12 @@ const DruxtNuxtModule = function(moduleOptions = {}) {
136
162
  this.options.components = (_a = this.options.components) != null ? _a : true;
137
163
  this.options.cli.badgeMessages.push(`${chalk.blue.bold("Druxt")} @ v${meta.version}`);
138
164
  this.options.cli.badgeMessages.push(`${chalk.bold("API:")} ${chalk.blue.underline(options.baseUrl + options.endpoint)}`);
165
+ this.nuxt.hook("storybook:config", async ({ stories }) => {
166
+ stories.push("druxt/stories/*.stories.mdx");
167
+ stories.push("druxt/dist/components/*.stories.mdx");
168
+ stories.push("druxt/dist/components/*.stories.mjs");
169
+ });
139
170
  };
140
- DruxtNuxtModule.meta = meta;
141
171
 
142
172
  class DruxtClient {
143
173
  constructor(baseUrl, options = {}) {
@@ -147,14 +177,15 @@ class DruxtClient {
147
177
  if (!baseUrl) {
148
178
  throw new Error("The 'baseUrl' parameter is required.");
149
179
  }
150
- let axiosSettings = {};
151
- if (baseUrl && !(options.proxy || {}).api) {
152
- axiosSettings.baseURL = baseUrl;
153
- }
154
- if (typeof options.axios === "object") {
155
- axiosSettings = Object.assign(axiosSettings, options.axios);
180
+ this.axios = {};
181
+ if (typeof options.axios === "function") {
182
+ this.axios = options.axios;
183
+ } else {
184
+ this.axios = axios.create({
185
+ ...options.axios || {},
186
+ baseURL: baseUrl
187
+ });
156
188
  }
157
- this.axios = axios.create(axiosSettings);
158
189
  if (options.debug) {
159
190
  const log = this.log;
160
191
  this.axios.interceptors.request.use((config) => {
@@ -206,9 +237,16 @@ class DruxtClient {
206
237
  }
207
238
  }
208
239
  if (Object.keys(permissions).length) {
209
- throw new TypeError(`${res.data.meta.omitted.detail}
210
-
211
- Required permissions: ${Object.keys(permissions).join(", ")}.`);
240
+ const err = {
241
+ response: {
242
+ statusText: res.data.meta.omitted.detail,
243
+ data: {
244
+ errors: [{ detail: `Required permissions:
245
+ - ${Object.keys(permissions).join("\n - ")}` }]
246
+ }
247
+ }
248
+ };
249
+ throw err;
212
250
  }
213
251
  }
214
252
  }
@@ -228,19 +266,49 @@ class DruxtClient {
228
266
  }
229
267
  });
230
268
  } catch (err) {
231
- response = (err.response || {}).data || err.message;
269
+ this.error(err, { url: href });
232
270
  }
233
271
  return response;
234
272
  }
273
+ error(err, context = {}) {
274
+ let { url } = context;
275
+ if (!url && ((err.response || {}).config || {}).url) {
276
+ url = err.response.config.url;
277
+ }
278
+ const title = [
279
+ (err.response || {}).status,
280
+ (err.response || {}).statusText || err.message
281
+ ].filter((s) => s).join(": ");
282
+ const meta = { url: url && [this.options.baseUrl, url].join("") };
283
+ let message = [title];
284
+ if (Object.values(meta).filter((o) => o).length) {
285
+ message.push(Object.entries(meta).filter(([, v]) => v).map(([key, value]) => `${key.toUpperCase()}: ${value}`).join("\n"));
286
+ }
287
+ if (((((err.response || {}).data || {}).errors || [])[0] || {}).detail) {
288
+ message.push(err.response.data.errors[0].detail);
289
+ }
290
+ const error = Error(message.join("\n\n"));
291
+ error.response = err.response;
292
+ error.druxt = context;
293
+ throw error;
294
+ }
295
+ async get(url, options) {
296
+ try {
297
+ const res = await this.axios.get(url, options);
298
+ this.checkPermissions(res);
299
+ return res;
300
+ } catch (err) {
301
+ this.error(err, { url });
302
+ }
303
+ }
235
304
  async getCollection(type, query) {
236
305
  const { href } = await this.getIndex(type);
237
306
  if (!href) {
238
307
  return false;
239
308
  }
240
309
  const url = this.buildQueryUrl(href, query);
241
- const res = await this.axios.get(url);
242
- this.checkPermissions(res);
243
- return res.data;
310
+ const { data } = await this.get(url);
311
+ return data;
244
312
  }
245
313
  async getCollectionAll(type, query) {
246
314
  const collections = [];
@@ -260,16 +328,27 @@ class DruxtClient {
260
328
  if (this.index && resource) {
261
329
  return this.index[resource] ? this.index[resource] : false;
262
330
  }
263
- let index = ((await this.axios.get(this.options.endpoint) || {}).data || {}).links;
331
+ const url = this.options.endpoint;
332
+ const { data } = await this.get(url);
333
+ let index = data.links;
334
+ if (typeof index !== "object") {
335
+ const err = { response: { statusText: "Invalid JSON:API endpoint" } };
336
+ this.error(err, { url });
337
+ }
264
338
  const baseUrl = this.options.baseUrl;
265
339
  index = Object.fromEntries(Object.entries(index).map(([key, value]) => {
266
340
  value.href = value.href.replace(baseUrl, "");
267
341
  return [key, value];
268
342
  }));
269
343
  if (index[this.options.jsonapiResourceConfig]) {
270
- const resources = await this.axios.get(index[this.options.jsonapiResourceConfig].href);
271
- for (const resourceType in resources.data.data) {
272
- const resource2 = resources.data.data[resourceType];
344
+ let resources = [];
345
+ try {
346
+ resources = (await this.get(index[this.options.jsonapiResourceConfig].href)).data.data;
347
+ } catch (err) {
348
+ this.log.warn(err.message);
349
+ }
350
+ for (const resourceType in resources) {
351
+ const resource2 = resources[resourceType];
273
352
  const internal = resource2.attributes.drupal_internal__id.split("--");
274
353
  const item = {
275
354
  resourceType: resource2.attributes.resourceType,
@@ -285,11 +364,7 @@ class DruxtClient {
285
364
  }
286
365
  }
287
366
  this.index = index;
288
- if (resource) {
289
- const response = this.index[resource] ? this.index[resource] : false;
290
- return response;
291
- }
292
- return this.index;
367
+ return resource ? this.index[resource] || false : this.index;
293
368
  }
294
369
  async getRelated(type, id, related, query) {
295
370
  if (!id || !type || !related) {
@@ -300,12 +375,8 @@ class DruxtClient {
300
375
  href = this.options.endpoint + "/" + type.replace("--", "/");
301
376
  }
302
377
  const url = this.buildQueryUrl(`${href}/${id}/${related}`, query);
303
- try {
304
- const related2 = await this.axios.get(url);
305
- return related2.data;
306
- } catch (e) {
307
- return false;
308
- }
378
+ const { data } = await this.get(url);
379
+ return data;
309
380
  }
310
381
  async getResource(type, id, query) {
311
382
  if (!id || !type) {
@@ -315,13 +386,9 @@ class DruxtClient {
315
386
  if (!href) {
316
387
  href = this.options.endpoint + "/" + type.replace("--", "/");
317
388
  }
318
- const url = this.buildQueryUrl(`${href}/${id}`, query);
319
- try {
320
- const resource = await this.axios.get(url);
321
- return resource.data;
322
- } catch (e) {
323
- return false;
324
- }
389
+ const url = this.buildQueryUrl([href, id].join("/"), query);
390
+ const { data } = await this.get(url);
391
+ return data;
325
392
  }
326
393
  async updateResource(resource) {
327
394
  const { href } = await this.getIndex(resource.type);
@@ -337,7 +404,7 @@ class DruxtClient {
337
404
  }
338
405
  });
339
406
  } catch (err) {
340
- response = (err.response || {}).data || err.message;
407
+ this.error(err);
341
408
  }
342
409
  return response;
343
410
  }
@@ -348,7 +415,7 @@ const getDrupalJsonApiParams = (query) => {
348
415
  };
349
416
 
350
417
  const dehydrateResources = ({ commit, queryObject, resources }) => {
351
- return resources.map((data) => {
418
+ return (resources || []).map((data) => {
352
419
  const link = decodeURI(((data.links || {}).self || {}).href || "");
353
420
  const href = typeof (queryObject.fields || {})[data.type] === "string" ? [link.split("?")[0], `fields[${data.type}]=${queryObject.fields[data.type]}`].join("?") : link;
354
421
  commit("druxt/addResource", {
@@ -539,5 +606,7 @@ class DruxtClass {
539
606
  }
540
607
  }
541
608
 
609
+ DruxtNuxtModule.meta = require("../package.json");
610
+
542
611
  export default DruxtNuxtModule;
543
612
  export { DruxtClass, DruxtClient, DruxtStore };