proca 1.8.2 → 2.0.1

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.
Files changed (55) hide show
  1. package/README.md +347 -195
  2. package/package.json +2 -1
  3. package/proca-cli +2 -2
  4. package/src/commands/action/add.mjs +131 -131
  5. package/src/commands/action/confirm.mjs +44 -44
  6. package/src/commands/action/count.mjs +41 -41
  7. package/src/commands/action/list.mjs +130 -130
  8. package/src/commands/action/replay.mjs +30 -30
  9. package/src/commands/action/requeue.mjs +110 -110
  10. package/src/commands/campaign/add.mjs +92 -83
  11. package/src/commands/campaign/copy.mjs +91 -0
  12. package/src/commands/campaign/delete.mjs +36 -56
  13. package/src/commands/campaign/get.mjs +5 -0
  14. package/src/commands/campaign/list.mjs +98 -111
  15. package/src/commands/campaign/queries.graphql +14 -14
  16. package/src/commands/campaign/status.mjs +39 -39
  17. package/src/commands/campaign/widget/archive.mjs +124 -0
  18. package/src/commands/campaign/widget/copy.mjs +175 -0
  19. package/src/commands/config/add.mjs +78 -78
  20. package/src/commands/config/folder.mjs +30 -30
  21. package/src/commands/config/server.mjs +15 -15
  22. package/src/commands/config/set.mjs +84 -84
  23. package/src/commands/config/user.mjs +50 -48
  24. package/src/commands/contact/area/count.mjs +38 -0
  25. package/src/commands/contact/count.mjs +0 -1
  26. package/src/commands/contact/list.mjs +128 -132
  27. package/src/commands/org/add.mjs +51 -51
  28. package/src/commands/org/crm.mjs +61 -61
  29. package/src/commands/org/delete.mjs +31 -31
  30. package/src/commands/org/email.mjs +94 -66
  31. package/src/commands/org/get.mjs +9 -1
  32. package/src/commands/service/add.mjs +59 -59
  33. package/src/commands/service/list.mjs +15 -15
  34. package/src/commands/target/add.mjs +52 -52
  35. package/src/commands/template/add.mjs +67 -67
  36. package/src/commands/template/list.mjs +33 -33
  37. package/src/commands/user/get.mjs +60 -60
  38. package/src/commands/user/invite.mjs +37 -37
  39. package/src/commands/user/join.mjs +51 -51
  40. package/src/commands/user/leave.mjs +47 -47
  41. package/src/commands/user/reset.mjs +72 -72
  42. package/src/commands/widget/add.mjs +61 -70
  43. package/src/commands/widget/delete.mjs +27 -27
  44. package/src/commands/widget/external/update.mjs +52 -0
  45. package/src/commands/widget/get.mjs +5 -0
  46. package/src/commands/widget/list.mjs +7 -5
  47. package/src/commands/widget/update.mjs +174 -0
  48. package/src/config.mjs +31 -31
  49. package/src/generated/schema.json +10675 -10675
  50. package/src/hooks/help.mjs +9 -9
  51. package/src/hooks/init.mjs +26 -26
  52. package/src/procaCommand.mjs +32 -9
  53. package/src/urql.mjs +39 -39
  54. package/src/util/twitter.mjs +19 -19
  55. package/theme.json +27 -27
@@ -1,14 +1,11 @@
1
1
  import { Flags } from "@oclif/core";
2
- import CampaignGet from "#src/commands/campaign/get.mjs";
3
- import OrgGet from "#src/commands/org/get.mjs";
2
+ import { getCampaign } from "#src/commands/campaign/get.mjs";
3
+ import { getOrg } from "#src/commands/org/get.mjs";
4
4
  import Command from "#src/procaCommand.mjs";
5
5
  import { gql, mutation } from "#src/urql.mjs";
6
6
 
7
7
  export default class WidgetAdd extends Command {
8
- //static args = { path: { description: "" } };
9
-
10
8
  static flags = {
11
- // flag with no value (-f, --force)
12
9
  ...super.globalFlags,
13
10
  campaign: Flags.string({
14
11
  char: "c",
@@ -23,8 +20,8 @@ export default class WidgetAdd extends Command {
23
20
  }),
24
21
  lang: Flags.string({
25
22
  char: "l",
26
- description: "language",
27
23
  default: "en",
24
+ description: "language",
28
25
  helpValue: "<en>",
29
26
  }),
30
27
  name: Flags.string({
@@ -35,90 +32,84 @@ export default class WidgetAdd extends Command {
35
32
  };
36
33
 
37
34
  create = async (flag) => {
38
- const orgName = flag.org;
39
- let campaign = { org: { name: orgName } }; // no need to fetch the campaign if the orgName is specified
40
- let org = {}; // no need to fetch the campaign if the orgName is specified
35
+ let campaign = null;
36
+
37
+ if (!flag.org) {
38
+ campaign = await getCampaign({ name: flag.campaign });
39
+ if (!campaign) {
40
+ throw new Error(`campaign not found: ${flag.campaign}`);
41
+ }
42
+ flag.org = campaign.org.name;
43
+ }
44
+
45
+ const org = await getOrg({
46
+ name: flag.org,
47
+ campaigns: false,
48
+ keys: false,
49
+ });
50
+
51
+ const input = {
52
+ name: flag.name ?? `${flag.campaign}/${flag.org}/${flag.lang}`,
53
+ locale: flag.lang,
54
+ };
41
55
 
42
- const addWidgetDocument = gql`
56
+ if (flag.config) {
57
+ input.config =
58
+ typeof flag.config === "string" ? JSON.parse(flag.config) : flag.config;
59
+ }
60
+
61
+ // else if (org?.config?.layout) {
62
+ // input.config = { layout: org.config.layout };
63
+ // }
64
+
65
+ // Optional ActionPage fields
66
+ if (flag.journey) input.journey = flag.journey;
67
+ if (flag.thankYouTemplate) {
68
+ input.thank_you_template = flag.thankYouTemplate;
69
+ }
70
+
71
+ if (flag.live !== undefined) input.live = flag.live;
72
+
73
+ const Document = gql`
43
74
  mutation addPage(
44
- $campaign: String!
45
- $org: String!
46
- $name: String!
47
- $lang: String!
48
- $config: Json
75
+ $campaignName: String!
76
+ $orgName: String!
77
+ $input: ActionPageInput!
49
78
  ) {
50
79
  addActionPage(
51
- campaignName: $campaign
52
- orgName: $org
53
- input: { name: $name, locale: $lang, config: $config }
80
+ campaignName: $campaignName
81
+ orgName: $orgName
82
+ input: $input
54
83
  ) {
55
84
  id
56
85
  }
57
86
  }
58
87
  `;
59
88
 
60
- if (!orgName) {
61
- try {
62
- const campapi = new CampaignGet();
63
- campaign = await campapi.fetch({ name: flag.campaign });
64
- flag.org = campaign.org.name;
65
- if (!flag.name) {
66
- flag.name = `${campaign.name}/${flag.lang}`;
67
- }
68
- } catch (e) {
69
- console.log("error", e);
70
- throw e;
71
- }
72
- }
73
-
74
- if (!campaign) {
75
- throw new Error(`campaign not found: ${flag.campaign}`);
76
- }
77
-
78
89
  try {
79
- const orgapi = new OrgGet();
80
- org = await orgapi.fetch({
81
- name: flag.org,
82
- campaigns: false,
83
- keys: false,
90
+ const r = await mutation(Document, {
91
+ campaignName: flag.campaign,
92
+ orgName: flag.org,
93
+ input,
84
94
  });
85
- if (org.config.layout) {
86
- flag.config = JSON.stringify({ layout: org.config.layout });
87
- }
88
- } catch (e) {
89
- console.log("error", e);
90
- throw e;
91
- }
92
-
93
- if (!flag.name) {
94
- flag.name = `${flag.campaign}/${flag.org}/${flag.lang}`;
95
- }
96
- try {
97
- const r = await mutation(addWidgetDocument, flag);
98
95
  return { id: r.addActionPage.id };
99
96
  } catch (e) {
100
- const errors = e.graphQLErrors;
101
- console.log(JSON.stringify(errors, null, 2), flag);
102
- if (errors[0].path[1] === "name") {
103
- this.error(`invalid name (already taken?): ${flag.name}`);
104
- throw new Error(errors[0].message);
97
+ const err = e.graphQLErrors?.[0];
98
+
99
+ if (err?.path?.[1] === "name") {
100
+ this.error(`invalid name (already taken?): ${input.name}`);
105
101
  }
106
- if (errors[0].extensions?.code === "permission_denied") {
107
- console.error("permission denied to create", name, campaign?.org.name);
108
- throw new Error(errors[0].message);
102
+
103
+ if (err?.extensions?.code === "permission_denied") {
104
+ this.error(`permission denied to create widget for org ${flag.org}`);
109
105
  }
110
- console.log(flag);
111
- // const page = await fetchByName(name);
112
- // console.warn("duplicate of widget", page.id);
113
- throw new Error(errors[0].message);
106
+
107
+ throw new Error(err?.message ?? "failed to create widget");
114
108
  }
115
109
  };
116
110
 
117
111
  async run() {
118
- const { args, flags } = await this.parse();
119
-
120
- // const org = { name: flags.twitter || flags.name, config: {} };
121
-
112
+ const { flags } = await this.parse();
122
113
  const data = await this.create(flags);
123
114
  return this.output(data);
124
115
  }
@@ -5,41 +5,41 @@ import Command from "#src/procaCommand.mjs";
5
5
  import { gql, mutation } from "#src/urql.mjs";
6
6
 
7
7
  export default class WidgetDelete extends Command {
8
- static description = "Delete a widget";
8
+ static description = "Delete a widget";
9
9
 
10
- static args = this.multiid();
10
+ static args = this.multiid();
11
11
 
12
- static flags = {
13
- ...this.flagify({ multiid: true }),
14
- };
12
+ static flags = {
13
+ ...this.flagify({ multiid: true }),
14
+ };
15
15
 
16
- delete = async (flags) => {
17
- const deletePageDocument = gql`
16
+ delete = async (flags) => {
17
+ const deletePageDocument = gql`
18
18
  mutation delete( $name:String!) {
19
19
  deleteActionPage(name: $name)
20
20
  }
21
21
  `;
22
22
 
23
- const r = await mutation(deletePageDocument, { name: flags.name });
24
- return { deleted: r.deleteActionPage };
25
- };
23
+ const r = await mutation(deletePageDocument, { name: flags.name });
24
+ return { deleted: r.deleteActionPage };
25
+ };
26
26
 
27
- table = (r) => {
28
- super.table(r, null, null);
29
- };
27
+ table = (r) => {
28
+ super.table(r, null, null);
29
+ };
30
30
 
31
- async run() {
32
- const { flags } = await this.parse(WidgetDelete);
33
- const wg = new WidgetGet([], this.config);
34
- const widget = await wg.fetch(flags);
35
- try {
36
- const data = await this.delete({ name: widget.name });
37
- widget.status = data.deleted;
38
- } catch (e) {
39
- widget.status = "can't delete widgets with actions";
40
- this.output(widget);
41
- this.error("a widget with actions can't be deleted");
42
- }
43
- return this.output(widget);
44
- }
31
+ async run() {
32
+ const { flags } = await this.parse(WidgetDelete);
33
+ const wg = new WidgetGet([], this.config);
34
+ const widget = await wg.fetch(flags);
35
+ try {
36
+ const data = await this.delete({ name: widget.name });
37
+ widget.status = data.deleted;
38
+ } catch (e) {
39
+ widget.status = "can't delete widgets with actions";
40
+ this.output(widget);
41
+ this.error("a widget with actions can't be deleted");
42
+ }
43
+ return this.output(widget);
44
+ }
45
45
  }
@@ -0,0 +1,52 @@
1
+ import { Flags } from "@oclif/core";
2
+ import Command from "#src/procaCommand.mjs";
3
+ import { gql, mutation } from "#src/urql.mjs";
4
+
5
+ export default class CounterUpdate extends Command {
6
+ static description =
7
+ "Update the global counter to add the actions collected elsewhere";
8
+
9
+ static examples = ["see also <%= config.bin %> contact count"];
10
+ static args = this.multiid();
11
+
12
+ static flags = {
13
+ ...this.flagify({ multiid: true }),
14
+ total: Flags.string({
15
+ char: "t",
16
+ description: "new total to include",
17
+ required: true,
18
+ parse: (input) => Number.parseInt(input, 10),
19
+ }),
20
+ };
21
+
22
+ async updateCounter(id, counter) {
23
+ const PushWidgetDocument = gql`
24
+ mutation updateActionPage($id: Int!, $counter: Int!) {
25
+ updateActionPage(id: $id, input: {extraSupporters:$counter}) {
26
+ id, name, locale
27
+ ...on PrivateActionPage { extraSupporters }
28
+ }
29
+ }`;
30
+ const r = await mutation(PushWidgetDocument, {
31
+ id,
32
+ counter,
33
+ });
34
+ if (r.errors) {
35
+ console.log(r);
36
+ console.log("check your config $npx proca config user");
37
+ throw new Error(r.errors[0].message || "can't update on the server");
38
+ }
39
+ return r.updateActionPage;
40
+ }
41
+
42
+ async run() {
43
+ const { flags } = await this.parse(CounterUpdate);
44
+
45
+ try {
46
+ const updated = await this.updateCounter(flags.id, flags.total);
47
+ return this.output(updated, { single: true });
48
+ } catch (error) {
49
+ this.error(error.message, { exit: 1 });
50
+ }
51
+ }
52
+ }
@@ -3,6 +3,11 @@ import Command from "#src/procaCommand.mjs";
3
3
  import { FragmentSummary } from "#src/queries/widget.mjs";
4
4
  import { gql, query } from "#src/urql.mjs";
5
5
 
6
+ export const getWidget = (params) => {
7
+ const d = new WidgetGet([]);
8
+ return d.fetch(params);
9
+ };
10
+
6
11
  export default class WidgetGet extends Command {
7
12
  static description = "view a widget";
8
13
 
@@ -4,6 +4,11 @@ import Command from "#src/procaCommand.mjs";
4
4
  import { FragmentSummary, FragmentSummaryOrg } from "#src/queries/widget.mjs";
5
5
  import { gql, query } from "#src/urql.mjs";
6
6
 
7
+ export const getWidgetList = (params) => {
8
+ const d = new WidgetList([]);
9
+ return d.fetch(params);
10
+ };
11
+
7
12
  export default class WidgetList extends Command {
8
13
  static description = "list all the widgets of an org or campaign";
9
14
 
@@ -19,6 +24,7 @@ export default class WidgetList extends Command {
19
24
  // exactlyOne: ["campaign", "org"], actually, we can filter on both
20
25
  description: "widgets of the organisation (coordinator or partner)",
21
26
  helpValue: "<organisation name>",
27
+ exactlyOne: ["campaign", "org"],
22
28
  // required: true,
23
29
  }),
24
30
  campaign: Flags.string({
@@ -120,12 +126,8 @@ ${FragmentSummaryOrg}
120
126
  };
121
127
 
122
128
  async run() {
123
- const { flags, args } = await this.parse(WidgetList);
129
+ const { flags } = await this.parse(WidgetList);
124
130
  let data = [];
125
- if (!flags.org && !flags.campaign) {
126
- const help = new Help(this.config);
127
- this.error("Must specify --org or --campaign or --help for more");
128
- }
129
131
  if (flags.org) data = await this.fetchOrg(flags.org);
130
132
  if (flags.campaign) data = await this.fetchCampaign(flags.campaign);
131
133
  return this.output(data);
@@ -0,0 +1,174 @@
1
+ import { Args, Flags } from "@oclif/core";
2
+ import { merge } from "merge-anything";
3
+ import WidgetGet from "#src/commands/widget/get.mjs";
4
+ import Command from "#src/procaCommand.mjs";
5
+ import { gql, mutation } from "#src/urql.mjs";
6
+
7
+ export default class WidgetUpdate extends Command {
8
+ static description = "Update a widget's properties";
9
+
10
+ static examples = [
11
+ "<%= config.bin %> <%= command.id %> 4454 --name new_widget_name",
12
+ "<%= config.bin %> <%= command.id %> 4454 --locale fr",
13
+ "<%= config.bin %> <%= command.id %> 4454 --confirm-optin",
14
+ "<%= config.bin %> <%= command.id %> 4454 --confirm-optin --dry-run",
15
+ ];
16
+
17
+ static args = this.multiid();
18
+
19
+ static flags = {
20
+ // flag with no value (-f, --force)
21
+ ...this.flagify({ multiid: true }),
22
+ rename: Flags.string({
23
+ char: "n",
24
+ description: "new name for the widget",
25
+ helpValue: "<widget name>",
26
+ }),
27
+
28
+ locale: Flags.string({
29
+ char: "l",
30
+ description: "change the locale",
31
+ helpValue: "<locale>",
32
+ }),
33
+ color: Flags.string({
34
+ description: "update color (not yet implemented)",
35
+ helpValue: "<hex code>",
36
+ }),
37
+ "confirm-optin": Flags.boolean({
38
+ description:
39
+ "add confirmOptIn (check email snack) to consent.email component ",
40
+
41
+ default: false,
42
+ }),
43
+ "confirm-action": Flags.boolean({
44
+ description:
45
+ "add actionConfirm (check email snack) to consent.email component ",
46
+
47
+ default: false,
48
+ }),
49
+
50
+ "dry-run": Flags.boolean({
51
+ description: "Show changes without updating the widget",
52
+ default: false,
53
+ }),
54
+ };
55
+
56
+ fetchWidget = async (params) => {
57
+ const widgetGet = new WidgetGet([], this.config);
58
+ return widgetGet.fetch(params);
59
+ };
60
+
61
+ update = async (widgetId, input) => {
62
+ console.log("Updating widget with input:", input);
63
+ const Document = gql`
64
+ mutation UpdateActionPage($id: Int!, $input: ActionPageInput!) {
65
+ updateActionPage(id: $id, input: $input) {
66
+ id
67
+ name
68
+ locale
69
+ config
70
+ thankYouTemplate
71
+ }
72
+ }
73
+ `;
74
+
75
+ const payload = {
76
+ ...input,
77
+ ...(input.config && typeof input.config !== "string"
78
+ ? { config: JSON.stringify(input.config) }
79
+ : {}),
80
+ };
81
+
82
+ const r = await mutation(Document, {
83
+ id: widgetId,
84
+ input: payload,
85
+ });
86
+
87
+ return r.updateActionPage;
88
+ };
89
+
90
+ table = (r) => {
91
+ super.table(r, null, null);
92
+ if (this.flags.config) {
93
+ this.prettyJson(r.config);
94
+ }
95
+ };
96
+
97
+ async run() {
98
+ const { flags } = await this.parse();
99
+ const {
100
+ id,
101
+ rename,
102
+ locale,
103
+ color,
104
+ "confirm-optin": confirmOptIn,
105
+ "confirm-action": confirmAction,
106
+ "dry-run": dryRun,
107
+ } = flags;
108
+
109
+ // Fetch current widget
110
+ const widget = await this.fetchWidget({ id });
111
+
112
+ if (!widget) {
113
+ this.error("Widget not found");
114
+ }
115
+
116
+ // Validate name
117
+ if (rename) {
118
+ const nameParts = renname.split("/");
119
+ if (nameParts.length < 2) {
120
+ this.error(
121
+ "Widget name must follow format: campaign_name/org_name or campaign_name/locale or campaign_name/org_name/locale",
122
+ );
123
+ }
124
+ }
125
+
126
+ const input = {
127
+ name: rename ?? widget.name,
128
+ locale: locale ?? widget.locale,
129
+ };
130
+
131
+ if (color) {
132
+ this.error(`Color update requested: ${color} (not yet implemented)`);
133
+ }
134
+ if (confirmOptIn || confirmAction) {
135
+ const act = confirmOptIn ? "confirmOptIn" : "confirmAction";
136
+ const currentConfig =
137
+ typeof widget.config === "string"
138
+ ? JSON.parse(widget.config)
139
+ : (widget.config ?? {});
140
+
141
+ const nextConfig = merge(currentConfig, {
142
+ component: {
143
+ consent: {
144
+ email: {
145
+ [act]: true,
146
+ },
147
+ },
148
+ },
149
+ });
150
+
151
+ input.config = nextConfig;
152
+
153
+ this.log(`✓ Will set consent.email.${act} = true`);
154
+
155
+ if (dryRun) {
156
+ this.log("\n--- DRY RUN ---\n");
157
+ this.log("FROM:");
158
+ this.log(JSON.stringify(currentConfig, null, 2));
159
+ this.log("\nTO:");
160
+ this.log(JSON.stringify(nextConfig, null, 2));
161
+ this.log("\n(no mutation executed)");
162
+ return;
163
+ }
164
+ }
165
+
166
+ try {
167
+ const updated = await this.update(widget.id, input);
168
+ this.log("✓ Widget updated successfully");
169
+ return this.output(updated);
170
+ } catch (err) {
171
+ this.error(`Failed to update widget: ${err.message}`);
172
+ }
173
+ }
174
+ }
package/src/config.mjs CHANGED
@@ -3,47 +3,47 @@ import { join } from "node:path";
3
3
  import { config as dotenv } from "dotenv";
4
4
 
5
5
  export const getFilename = (folder, env = "default") =>
6
- join(folder, `${env}.env`);
6
+ join(folder, `${env}.env`);
7
7
 
8
8
  export const load = (folder, env = "default") => {
9
- const file = getFilename(folder, env);
10
- const config = dotenv({ path: file });
9
+ const file = getFilename(folder, env);
10
+ const config = dotenv({ path: file });
11
11
 
12
- return {
13
- token: config.parsed.PROCA_TOKEN,
14
- url: config.parsed.REACT_APP_API_URL,
15
- folder: process.env.PROCA_CONFIG_FOLDER,
16
- };
12
+ return {
13
+ token: config.parsed.PROCA_TOKEN,
14
+ url: config.parsed.REACT_APP_API_URL,
15
+ folder: process.env.PROCA_CONFIG_FOLDER,
16
+ };
17
17
  };
18
18
 
19
19
  export const get = (file, parsed = false) => {
20
- try {
21
- if (parsed) {
22
- const config = dotenv({ path: file });
23
- return config.parsed;
24
- }
25
- const userConfig = readFileSync(file, "utf8");
26
- return userConfig;
27
- } catch (e) {
28
- if (e.code === "ENOENT") {
29
- return undefined;
30
- }
31
- throw e;
32
- }
20
+ try {
21
+ if (parsed) {
22
+ const config = dotenv({ path: file });
23
+ return config.parsed;
24
+ }
25
+ const userConfig = readFileSync(file, "utf8");
26
+ return userConfig;
27
+ } catch (e) {
28
+ if (e.code === "ENOENT") {
29
+ return undefined;
30
+ }
31
+ throw e;
32
+ }
33
33
  };
34
34
 
35
35
  export const write = (file, content) => {
36
- writeFileSync(file, content);
36
+ writeFileSync(file, content);
37
37
  };
38
38
 
39
39
  export const format = (obj) => {
40
- const content = ["# generated by proca-cli"];
41
- for (const [key, value] of Object.entries(obj)) {
42
- if (value) {
43
- content.push(`${key}='${value.replace(/'/g, "''")}'`);
44
- } else {
45
- content.push(`#${key}= `);
46
- }
47
- }
48
- return content.join("\n");
40
+ const content = ["# generated by proca-cli"];
41
+ for (const [key, value] of Object.entries(obj)) {
42
+ if (value) {
43
+ content.push(`${key}='${value.replace(/'/g, "''")}'`);
44
+ } else {
45
+ content.push(`#${key}= `);
46
+ }
47
+ }
48
+ return content.join("\n");
49
49
  };