& {
+ cta?: CTA;
+ title: string;
+ };
+};
-type MailOptions = {
+type MailOptions = TemplateContext & {
subject?: string;
- text?: string;
- html?: string;
attachments?: Mail.Attachment[];
signOff?: string;
};
-export const sendEmail = async (to: string | string[], mailOptions: MailOptions = {}) => {
- const {
- subject,
- text,
- html,
- attachments,
- signOff = html ? HTML_SIGN_OFF : TEXT_SIGN_OFF,
- } = mailOptions;
+const compileHtml = (context: TemplateContext) => {
+ const { templateName, templateContext } = context;
+ const templatePath = path.resolve(__dirname, './templates/wrapper.html');
+ const mainTemplate = fs.readFileSync(templatePath);
+ const compiledTemplate = handlebars.compile(mainTemplate.toString());
+ let content = '';
+ if (templateName) {
+ const innerContentTemplate = fs.readFileSync(
+ path.resolve(__dirname, `./templates/content/${templateName}.html`),
+ );
+ content = handlebars.compile(innerContentTemplate.toString())(templateContext);
+ }
+ return compiledTemplate({
+ ...templateContext,
+ content,
+ }).toString();
+};
+
+export const sendEmail = async (to: string | string[], mailOptions: MailOptions) => {
+ const { subject, templateName, templateContext, attachments, signOff } = mailOptions || {};
const SMTP_HOST = getEnvVarOrDefault('SMTP_HOST', undefined);
const SMTP_USER = getEnvVarOrDefault('SMTP_USER', undefined);
const SMTP_PASSWORD = getEnvVarOrDefault('SMTP_PASSWORD', undefined);
const SITE_EMAIL_ADDRESS = getEnvVarOrDefault('SITE_EMAIL_ADDRESS', undefined);
- if (text && html) {
- throw new Error('Only text or HTML can be sent in an email, not both');
- }
-
if (!SMTP_HOST || !SMTP_USER || !SMTP_PASSWORD || !SITE_EMAIL_ADDRESS) {
return {};
}
@@ -52,8 +71,7 @@ export const sendEmail = async (to: string | string[], mailOptions: MailOptions
// Make sure it doesn't send real users mail from the dev server
const sendTo = getIsProductionEnvironment() ? to : (requireEnv('DEV_EMAIL_ADDRESS') as string);
- const fullText = text ? `${text}\n${signOff}` : undefined;
- const fullHtml = html ? `${html}
${signOff}` : undefined;
+ const fullHtml = compileHtml({ templateName, templateContext, signOff });
return transporter.sendMail({
from: `Tupaia <${SITE_EMAIL_ADDRESS}>`,
@@ -61,7 +79,6 @@ export const sendEmail = async (to: string | string[], mailOptions: MailOptions
to: sendTo,
subject,
attachments,
- text: fullText,
html: fullHtml,
});
};
diff --git a/packages/server-utils/src/email/templates/content/dashboardSubscription.html b/packages/server-utils/src/email/templates/content/dashboardSubscription.html
new file mode 100644
index 0000000000..2011de78f9
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/dashboardSubscription.html
@@ -0,0 +1,3 @@
+
+
The latest data for the {{dashboardName}} dashboard in {{entityName}} is ready to view.
+
diff --git a/packages/server-utils/src/email/templates/content/deleteAccount.html b/packages/server-utils/src/email/templates/content/deleteAccount.html
new file mode 100644
index 0000000000..4ed54a4d60
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/deleteAccount.html
@@ -0,0 +1,6 @@
+
+
+ {{user.first_name}} {{user.last_name}} ({{user.email}} - {{user.id}}, {{user.position}} at
+ {{user.employer}}) has requested to delete their account.
+
+
diff --git a/packages/server-utils/src/email/templates/content/emailAfterTimeout.html b/packages/server-utils/src/email/templates/content/emailAfterTimeout.html
new file mode 100644
index 0000000000..d9c2b24310
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/emailAfterTimeout.html
@@ -0,0 +1,4 @@
+
+
Hi {{userName}},
+
{{message}}
+
diff --git a/packages/server-utils/src/email/templates/content/overdueTask.html b/packages/server-utils/src/email/templates/content/overdueTask.html
new file mode 100644
index 0000000000..96a1f4a680
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/overdueTask.html
@@ -0,0 +1,12 @@
+
+
Hi {{userName}},
+
+ Oh no! Looks like you have an overdue task.
+
+
+ This is just to let you know that you have an overdue task. The task is {{surveyName}} for {{entityName}}. To view and complete your tasks head to DataTrak.
+
+
+ Have fun using the platform and feel free to get in touch if you have any questions.
+
+
diff --git a/packages/server-utils/src/email/templates/content/passwordReset.html b/packages/server-utils/src/email/templates/content/passwordReset.html
new file mode 100644
index 0000000000..e044eab6ef
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/passwordReset.html
@@ -0,0 +1,11 @@
+
+
Hi {{userName}}
+
+ You are receiving this email because someone requested a password reset for this user account on
+ Tupaia.org. To reset your password follow the link below.
+
+
+ If you believe this email was sent to you in error, please contact us immediately at
+ admin@tupaia.org.
+
+
diff --git a/packages/server-utils/src/email/templates/content/permissionGranted.html b/packages/server-utils/src/email/templates/content/permissionGranted.html
new file mode 100644
index 0000000000..3bb14d6860
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/permissionGranted.html
@@ -0,0 +1,13 @@
+
+
Hi {{userName}}
+
+ This is just to let you know that you've been added to the {{permissionGroupName}} access group
+ for for {{entityName}}.
+
+
{{{description}}}
+
+ Please note that you'll need to log out and then log back in to get access to the new
+ permissions.
+
+
Feel free to get in touch if you have any questions.
+
diff --git a/packages/server-utils/src/email/templates/content/requestCountryAccess.html b/packages/server-utils/src/email/templates/content/requestCountryAccess.html
new file mode 100644
index 0000000000..49f7786320
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/requestCountryAccess.html
@@ -0,0 +1,19 @@
+
+
+ {{user.first_name}} {{user.last_name}} ({{user.email}} - {{user.id}}, {{user.position}} at
+ {{user.employer}}) has requested access to countries:
+
+
+ {{#each countries}}
+ - {{this}}
+ {{/each}}
+
+ {{#if project}}
+
+ For the project {{project.code}} (linked to permission groups: {{project.permissionGroups}})
+
+ {{/if}}
+ {{#if message}}
+
With the message: "{{message}}"
+ {{/if}}
+
diff --git a/packages/server-utils/src/email/templates/content/taskAssigned.html b/packages/server-utils/src/email/templates/content/taskAssigned.html
new file mode 100644
index 0000000000..f9c15dcf8a
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/taskAssigned.html
@@ -0,0 +1,9 @@
+
+
Hi {{userName}},
+
+ This is just to let you know that you've been assigned a new task. The task is {{surveyName}}
+ for {{entityName}}. To view and complete your tasks head to
+ DataTrak.
+
+
Have fun using the platform and feel free to get in touch if you have any questions.
+
diff --git a/packages/server-utils/src/email/templates/content/verifyEmail.html b/packages/server-utils/src/email/templates/content/verifyEmail.html
new file mode 100644
index 0000000000..a1ba2e92f1
--- /dev/null
+++ b/packages/server-utils/src/email/templates/content/verifyEmail.html
@@ -0,0 +1,8 @@
+
+
Thank you for registering with {{platform}}
+
Please click below to register your email address.
+
+ If you believe this email was sent to you in error, please contact us immediately at
+ admin@tupaia.org.
+
+
diff --git a/packages/server-utils/src/email/templates/wrapper.html b/packages/server-utils/src/email/templates/wrapper.html
new file mode 100644
index 0000000000..2bf0b86fe5
--- /dev/null
+++ b/packages/server-utils/src/email/templates/wrapper.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+ |
+ |
+
+
+
+ {{title}}
+ |
+
+
+ {{{content}}} |
+
+ {{#if cta}}
+
+
+ {{cta.text}}
+ |
+
+ {{/if}}
+
+
+ {{#if signoff}}
+ {{signoff}}
+ {{else}}
+
+ Cheers,
+
+ The Tupaia Team
+
+ {{/if}}
+
+ |
+
+
+
+
+ tupaia.org
+ bes.au
+
+ Beyond Essential Systems
+
+ 89 Nicholson St, Brunswick East VIC 3057 Australia
+
+ |
+
+ {{#if unsubscribeUrl}}
+
+
+
+ If you wish to unsubscribe from these emails please click
+ here
+
+ |
+
+ {{/if}}
+
+
+
+
+
diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts
index 5c78cfa9e7..3d6fcc3cc1 100644
--- a/packages/server-utils/src/index.ts
+++ b/packages/server-utils/src/index.ts
@@ -1,6 +1,6 @@
export { downloadPageAsPDF } from './downloadPageAsPDF';
export * from './s3';
-export { sendEmail } from './sendEmail';
+export { sendEmail } from './email';
export { generateUnsubscribeToken, verifyUnsubscribeToken } from './unsubscribeToken';
export { configureDotEnv } from './configureDotEnv';
export { constructExportEmail } from './constructExportEmail';
diff --git a/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts b/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts
index 27f67b7a74..fc90fc2217 100644
--- a/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts
+++ b/packages/tupaia-web-server/src/routes/EmailDashboardRoute.ts
@@ -112,7 +112,6 @@ export class EmailDashboardRoute extends Route {
const emails = mailingListEntries.map(({ email }) => email);
const subject = `Tupaia Dashboard: ${projectEntity.name} ${entity.name} ${dashboard.name}`;
- const html = `Latest data for the ${dashboard.name} dashboard in ${entity.name}.
`;
const filename = `${projectEntity.name}-${entity.name}-${dashboard.name}-export.pdf`;
emails.forEach(email => {
@@ -122,13 +121,16 @@ export class EmailDashboardRoute extends Route {
token: unsubscribeToken,
mailingListId: mailingList.id,
});
- const unsubscribeHtml = `If you wish to unsubscribe from these emails please click here`;
- const signOff = `Cheers,
The Tupaia Team
${unsubscribeHtml}
`;
return sendEmail(email, {
subject,
- html,
- signOff,
attachments: [{ filename, content: buffer }],
+ templateName: 'dashboardSubscription',
+ templateContext: {
+ title: 'Your Tupaia Dashboard Export is ready',
+ dashboardName: dashboard.name,
+ entityName: entity.name,
+ unsubscribeUrl,
+ },
});
});
diff --git a/packages/tupaia-web/src/api/queries/useEntitySearch.ts b/packages/tupaia-web/src/api/queries/useEntitySearch.ts
index facd47d31c..8835d6adf0 100644
--- a/packages/tupaia-web/src/api/queries/useEntitySearch.ts
+++ b/packages/tupaia-web/src/api/queries/useEntitySearch.ts
@@ -4,9 +4,9 @@
*/
import { useQuery } from '@tanstack/react-query';
+import { useDebounce } from '@tupaia/ui-components';
import { ProjectCode, Entity } from '../../types';
import { get } from '../api';
-import { useDebounce } from '../../utils';
export const useEntitySearch = (
projectCode?: ProjectCode,
diff --git a/packages/tupaia-web/src/utils/index.ts b/packages/tupaia-web/src/utils/index.ts
index 095192f6d4..db7856f586 100644
--- a/packages/tupaia-web/src/utils/index.ts
+++ b/packages/tupaia-web/src/utils/index.ts
@@ -10,7 +10,6 @@ export { useEntityLink } from './useEntityLink';
export { useDateRanges, convertDateRangeToUrlPeriodString } from './useDateRanges';
export { gaEvent } from './ga';
export { transformDownloadLink } from './transformDownloadLink';
-export { useDebounce } from './useDebounce';
export { getDefaultDashboard } from './getDefaultDashboard';
export { useGAEffect } from './useGAEffect';
export { useUrlLoginToken } from './useUrlLoginToken';
diff --git a/packages/types/config/models/config.json b/packages/types/config/models/config.json
index 3dd7b0cbf6..4449c86100 100644
--- a/packages/types/config/models/config.json
+++ b/packages/types/config/models/config.json
@@ -23,7 +23,9 @@
"public.entity.attributes": "EntityAttributes",
"public.user_account.preferences": "UserAccountPreferences",
"public.dashboard_relation.entity_types": "EntityType[]",
- "public.project.config": "ProjectConfig"
+ "public.project.config": "ProjectConfig",
+ "public.task_comment.template_variables": "TaskCommentTemplateVariables",
+ "public.task.repeat_schedule": "RepeatSchedule"
},
"typeMap": {
"string": ["geography"],
@@ -37,8 +39,10 @@
"MapOverlayConfig": "./models-extra",
"EntityAttributes": "./models-extra",
"UserAccountPreferences": "./models-extra",
+ "EntityType": "./models-extra",
"ProjectConfig": "./models-extra",
- "EntityType": "./models-extra"
+ "TaskCommentTemplateVariables": "./models-extra",
+ "RepeatSchedule": "./models-extra"
}
}
}
diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts
index d7942c0c75..36de1737fa 100644
--- a/packages/types/src/schemas/schemas.ts
+++ b/packages/types/src/schemas/schemas.ts
@@ -38838,6 +38838,90 @@ export const ArithmeticQuestionConfigSchema = {
]
}
+export const UserQuestionConfigSchema = {
+ "type": "object",
+ "properties": {
+ "permissionGroup": {
+ "description": "Filters the users by permission group.",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "permissionGroup"
+ ]
+}
+
+export const TaskQuestionConfigSchema = {
+ "type": "object",
+ "properties": {
+ "shouldCreateTask": {
+ "description": "Determines if a task should be created.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ },
+ "entityId": {
+ "description": "Determines the entity that the task will be created for.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ },
+ "surveyCode": {
+ "description": "Determines the survey that the task will be created for.",
+ "type": "string"
+ },
+ "dueDate": {
+ "description": "Determines the due date of the task.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ },
+ "assignee": {
+ "description": "Determines the assignee of the task.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "assignee",
+ "dueDate",
+ "entityId",
+ "shouldCreateTask",
+ "surveyCode"
+ ]
+}
+
export const SurveyScreenComponentConfigSchema = {
"type": "object",
"properties": {
@@ -39293,6 +39377,88 @@ export const SurveyScreenComponentConfigSchema = {
"required": [
"formula"
]
+ },
+ "user": {
+ "type": "object",
+ "properties": {
+ "permissionGroup": {
+ "description": "Filters the users by permission group.",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "permissionGroup"
+ ]
+ },
+ "task": {
+ "type": "object",
+ "properties": {
+ "shouldCreateTask": {
+ "description": "Determines if a task should be created.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ },
+ "entityId": {
+ "description": "Determines the entity that the task will be created for.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ },
+ "surveyCode": {
+ "description": "Determines the survey that the task will be created for.",
+ "type": "string"
+ },
+ "dueDate": {
+ "description": "Determines the due date of the task.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ },
+ "assignee": {
+ "description": "Determines the assignee of the task.",
+ "type": "object",
+ "properties": {
+ "questionId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "questionId"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "assignee",
+ "dueDate",
+ "entityId",
+ "shouldCreateTask",
+ "surveyCode"
+ ]
}
},
"additionalProperties": false
@@ -39549,6 +39715,193 @@ export const ProjectConfigSchema = {
"additionalProperties": false
}
+export const SystemCommentSubTypeSchema = {
+ "enum": [
+ "complete",
+ "create",
+ "overdue",
+ "update"
+ ],
+ "type": "string"
+}
+
+export const TaskUpdateCommentTemplateVariablesSchema = {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "update"
+ ]
+ },
+ "originalValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "newValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "field": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+}
+
+export const TaskCreateCommentTemplateVariablesSchema = {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "create"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+}
+
+export const TaskCompletedCommentTemplateVariablesSchema = {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "complete"
+ ]
+ },
+ "taskId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+}
+
+export const TaskCommentTemplateVariablesSchema = {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "update"
+ ]
+ },
+ "originalValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "newValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "field": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "create"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "complete"
+ ]
+ },
+ "taskId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ }
+ ]
+}
+
+export const RepeatScheduleSchema = {
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "freq": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ },
+ "bymonthday": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "bysetpos": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "dtstart": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+}
+
export const AccessRequestSchema = {
"type": "object",
"properties": {
@@ -39814,7 +40167,9 @@ export const AnalyticsSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
},
@@ -39879,7 +40234,9 @@ export const AnalyticsCreateSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
},
@@ -39944,7 +40301,9 @@ export const AnalyticsUpdateSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
},
@@ -79641,7 +80000,9 @@ export const QuestionSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
}
@@ -79704,7 +80065,9 @@ export const QuestionCreateSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
}
@@ -79769,7 +80132,9 @@ export const QuestionUpdateSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
}
@@ -80918,19 +81283,70 @@ export const TaskSchema = {
"assignee_id": {
"type": "string"
},
- "due_date": {
+ "created_at": {
"type": "string",
"format": "date-time"
},
+ "due_date": {
+ "type": "number"
+ },
"entity_id": {
"type": "string"
},
"id": {
"type": "string"
},
+ "initial_request_id": {
+ "type": "string"
+ },
+ "overdue_email_sent": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "parent_task_id": {
+ "type": "string"
+ },
"repeat_schedule": {
+ "additionalProperties": false,
"type": "object",
- "properties": {}
+ "properties": {
+ "freq": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ },
+ "bymonthday": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "bysetpos": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "dtstart": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
},
"status": {
"enum": [
@@ -80942,10 +81358,14 @@ export const TaskSchema = {
},
"survey_id": {
"type": "string"
+ },
+ "survey_response_id": {
+ "type": "string"
}
},
"additionalProperties": false,
"required": [
+ "created_at",
"entity_id",
"id",
"survey_id"
@@ -80958,16 +81378,67 @@ export const TaskCreateSchema = {
"assignee_id": {
"type": "string"
},
- "due_date": {
+ "created_at": {
"type": "string",
"format": "date-time"
},
+ "due_date": {
+ "type": "number"
+ },
"entity_id": {
"type": "string"
},
+ "initial_request_id": {
+ "type": "string"
+ },
+ "overdue_email_sent": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "parent_task_id": {
+ "type": "string"
+ },
"repeat_schedule": {
+ "additionalProperties": false,
"type": "object",
- "properties": {}
+ "properties": {
+ "freq": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ },
+ "bymonthday": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "bysetpos": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "dtstart": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
},
"status": {
"enum": [
@@ -80979,6 +81450,9 @@ export const TaskCreateSchema = {
},
"survey_id": {
"type": "string"
+ },
+ "survey_response_id": {
+ "type": "string"
}
},
"additionalProperties": false,
@@ -80994,19 +81468,70 @@ export const TaskUpdateSchema = {
"assignee_id": {
"type": "string"
},
- "due_date": {
+ "created_at": {
"type": "string",
"format": "date-time"
},
+ "due_date": {
+ "type": "number"
+ },
"entity_id": {
"type": "string"
},
"id": {
"type": "string"
},
+ "initial_request_id": {
+ "type": "string"
+ },
+ "overdue_email_sent": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "parent_task_id": {
+ "type": "string"
+ },
"repeat_schedule": {
+ "additionalProperties": false,
"type": "object",
- "properties": {}
+ "properties": {
+ "freq": {
+ "type": "number"
+ },
+ "interval": {
+ "type": "number"
+ },
+ "bymonthday": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "bysetpos": {
+ "anyOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "dtstart": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
},
"status": {
"enum": [
@@ -81018,6 +81543,318 @@ export const TaskUpdateSchema = {
},
"survey_id": {
"type": "string"
+ },
+ "survey_response_id": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
+
+export const TaskCommentSchema = {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "id": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "task_id": {
+ "type": "string"
+ },
+ "template_variables": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "update"
+ ]
+ },
+ "originalValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "newValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "field": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "create"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "complete"
+ ]
+ },
+ "taskId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "type": {
+ "enum": [
+ "system",
+ "user"
+ ],
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "user_name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "created_at",
+ "id",
+ "task_id",
+ "template_variables",
+ "type",
+ "user_name"
+ ]
+}
+
+export const TaskCommentCreateSchema = {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "message": {
+ "type": "string"
+ },
+ "task_id": {
+ "type": "string"
+ },
+ "template_variables": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "update"
+ ]
+ },
+ "originalValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "newValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "field": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "create"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "complete"
+ ]
+ },
+ "taskId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "type": {
+ "enum": [
+ "system",
+ "user"
+ ],
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "user_name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "task_id",
+ "user_name"
+ ]
+}
+
+export const TaskCommentUpdateSchema = {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "id": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "task_id": {
+ "type": "string"
+ },
+ "template_variables": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "update"
+ ]
+ },
+ "originalValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "newValue": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "field": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "create"
+ ]
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "complete"
+ ]
+ },
+ "taskId": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "type"
+ ]
+ }
+ ]
+ },
+ "type": {
+ "enum": [
+ "system",
+ "user"
+ ],
+ "type": "string"
+ },
+ "user_id": {
+ "type": "string"
+ },
+ "user_name": {
+ "type": "string"
}
},
"additionalProperties": false
@@ -81572,6 +82409,14 @@ export const TaskStatusSchema = {
"type": "string"
}
+export const TaskCommentTypeSchema = {
+ "enum": [
+ "system",
+ "user"
+ ],
+ "type": "string"
+}
+
export const SyncGroupSyncStatusSchema = {
"enum": [
"ERROR",
@@ -81614,7 +82459,9 @@ export const QuestionTypeSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
}
@@ -82258,7 +83105,9 @@ export const CamelCasedQuestionSchema = {
"Photo",
"PrimaryEntity",
"Radio",
- "SubmissionDate"
+ "SubmissionDate",
+ "Task",
+ "User"
],
"type": "string"
},
@@ -82345,6 +83194,23 @@ export const FileUploadAnswerSchema = {
]
}
+export const UserAnswerSchema = {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "name"
+ ]
+}
+
export const AnswersSchema = {
"type": "object",
"additionalProperties": false
@@ -82733,6 +83599,9 @@ export const EntityResponseSchema = {
},
"isRecent": {
"type": "boolean"
+ },
+ "parent_name": {
+ "type": "string"
}
},
"required": [
@@ -82745,6 +83614,376 @@ export const EntityResponseSchema = {
]
}
+export const AssigneeSchema = {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
+
+export const TaskResponseSchema = {
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "assigneeId": {
+ "type": "string"
+ },
+ "initialRequestId": {
+ "type": "string"
+ },
+ "overdueEmailSent": {
+ "type": "object",
+ "properties": {
+ "toString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toDateString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toTimeString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toLocaleString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toLocaleDateString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toLocaleTimeString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "valueOf": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getTime": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getFullYear": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCFullYear": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getMonth": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCMonth": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getDate": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCDate": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getDay": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCDay": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getHours": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCHours": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getMinutes": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCMinutes": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getSeconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCSeconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getMilliseconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getUTCMilliseconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getTimezoneOffset": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setTime": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setMilliseconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setUTCMilliseconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setSeconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setUTCSeconds": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setMinutes": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setUTCMinutes": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setHours": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setUTCHours": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setDate": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setUTCDate": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setMonth": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setUTCMonth": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setFullYear": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "setUTCFullYear": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toUTCString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toISOString": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "toJSON": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "getVarDate": {
+ "type": "object",
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "getDate",
+ "getDay",
+ "getFullYear",
+ "getHours",
+ "getMilliseconds",
+ "getMinutes",
+ "getMonth",
+ "getSeconds",
+ "getTime",
+ "getTimezoneOffset",
+ "getUTCDate",
+ "getUTCDay",
+ "getUTCFullYear",
+ "getUTCHours",
+ "getUTCMilliseconds",
+ "getUTCMinutes",
+ "getUTCMonth",
+ "getUTCSeconds",
+ "getVarDate",
+ "setDate",
+ "setFullYear",
+ "setHours",
+ "setMilliseconds",
+ "setMinutes",
+ "setMonth",
+ "setSeconds",
+ "setTime",
+ "setUTCDate",
+ "setUTCFullYear",
+ "setUTCHours",
+ "setUTCMilliseconds",
+ "setUTCMinutes",
+ "setUTCMonth",
+ "setUTCSeconds",
+ "toDateString",
+ "toISOString",
+ "toJSON",
+ "toLocaleDateString",
+ "toLocaleString",
+ "toLocaleTimeString",
+ "toString",
+ "toTimeString",
+ "toUTCString",
+ "valueOf"
+ ]
+ },
+ "parentTaskId": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "cancelled",
+ "completed",
+ "to_do"
+ ],
+ "type": "string"
+ },
+ "surveyResponseId": {
+ "type": "string"
+ },
+ "assignee": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "taskStatus": {
+ "enum": [
+ "cancelled",
+ "completed",
+ "overdue",
+ "repeating",
+ "to_do"
+ ],
+ "type": "string"
+ },
+ "survey": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "code": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "code",
+ "id",
+ "name"
+ ]
+ },
+ "entity": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "code": {
+ "type": "string"
+ },
+ "countryCode": {
+ "type": "string"
+ },
+ "parentName": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "code",
+ "countryCode",
+ "id",
+ "name"
+ ]
+ },
+ "repeatSchedule": {
+ "type": "object",
+ "additionalProperties": false
+ },
+ "taskDueDate": {
+ "type": "string",
+ "format": "date-time"
+ }
+ },
+ "required": [
+ "entity",
+ "survey",
+ "taskStatus"
+ ]
+}
+
+export const UserResponseSchema = {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "name"
+ ]
+}
+
export const MailingListSchema = {
"type": "object",
"properties": {
diff --git a/packages/types/src/types/index.ts b/packages/types/src/types/index.ts
index f0eb9e6d88..1206c042a3 100644
--- a/packages/types/src/types/index.ts
+++ b/packages/types/src/types/index.ts
@@ -98,6 +98,11 @@ export {
EntityQuestionConfigFieldValue,
EntityQuestionConfigFieldKey,
ProjectConfig,
+ TaskQuestionConfig,
+ UserQuestionConfig,
+ SystemCommentSubType,
+ TaskCommentTemplateVariables,
+ RepeatSchedule,
EntityType,
} from './models-extra';
export * from './requests';
diff --git a/packages/types/src/types/models-extra/index.ts b/packages/types/src/types/models-extra/index.ts
index f58df72194..bb7484a22f 100644
--- a/packages/types/src/types/models-extra/index.ts
+++ b/packages/types/src/types/models-extra/index.ts
@@ -96,6 +96,8 @@ export {
EntityQuestionConfigFields,
EntityQuestionConfigFieldValue,
EntityQuestionConfigFieldKey,
+ TaskQuestionConfig,
+ UserQuestionConfig,
} from './survey';
export { LeaderboardItem } from './leaderboard';
export {
@@ -108,4 +110,5 @@ export { VizPeriodGranularity, DashboardItemType } from './common';
export { isChartReport, isViewReport, isMatrixReport } from './report';
export { UserAccountPreferences } from './user';
export { ProjectConfig } from './project';
+export { RepeatSchedule, TaskCommentTemplateVariables, SystemCommentSubType } from './task';
export { EntityType } from './entityType';
diff --git a/packages/types/src/types/models-extra/survey/index.ts b/packages/types/src/types/models-extra/survey/index.ts
index c3f4c7eddb..2f60c9a936 100644
--- a/packages/types/src/types/models-extra/survey/index.ts
+++ b/packages/types/src/types/models-extra/survey/index.ts
@@ -13,4 +13,6 @@ export {
EntityQuestionConfigFields,
EntityQuestionConfigFieldValue,
EntityQuestionConfigFieldKey,
+ TaskQuestionConfig,
+ UserQuestionConfig,
} from './surveyScreenComponent';
diff --git a/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts b/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts
index 3af3ed3f9b..ec1aaafdf7 100644
--- a/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts
+++ b/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts
@@ -3,8 +3,8 @@
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/
-import { Entity, Question } from '../../models';
import { EntityType } from '../entityType';
+import { Entity, PermissionGroup, Question, Survey } from '../../models';
export type CodeGeneratorQuestionConfig = {
type: 'shortid' | 'mongoid';
@@ -70,10 +70,42 @@ export type ArithmeticQuestionConfig = {
>;
};
+export type UserQuestionConfig = {
+ /**
+ * @description Filters the users by permission group.
+ */
+ permissionGroup: PermissionGroup['id'];
+};
+
+export type TaskQuestionConfig = {
+ /**
+ * @description Determines if a task should be created.
+ */
+ shouldCreateTask: QuestionValue;
+ /**
+ * @description Determines the entity that the task will be created for.
+ */
+ entityId: QuestionValue;
+ /**
+ * @description Determines the survey that the task will be created for.
+ */
+ surveyCode: Survey['code'];
+ /**
+ * @description Determines the due date of the task.
+ */
+ dueDate: QuestionValue;
+ /**
+ * @description Determines the assignee of the task.
+ */
+ assignee: QuestionValue;
+};
+
export type SurveyScreenComponentConfig = {
codeGenerator?: CodeGeneratorQuestionConfig;
autocomplete?: AutocompleteQuestionConfig;
entity?: EntityQuestionConfig;
condition?: ConditionQuestionConfig;
arithmetic?: ArithmeticQuestionConfig;
+ user?: UserQuestionConfig;
+ task?: TaskQuestionConfig;
};
diff --git a/packages/types/src/types/models-extra/task.ts b/packages/types/src/types/models-extra/task.ts
new file mode 100644
index 0000000000..e9121fd8a7
--- /dev/null
+++ b/packages/types/src/types/models-extra/task.ts
@@ -0,0 +1,42 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import { Task } from '../models';
+
+export enum SystemCommentSubType {
+ update = 'update',
+ create = 'create',
+ overdue = 'overdue',
+ complete = 'complete',
+}
+
+export type TaskUpdateCommentTemplateVariables = {
+ type: SystemCommentSubType.update;
+ originalValue?: string | number;
+ newValue?: string | number;
+ field?: string;
+};
+
+export type TaskCreateCommentTemplateVariables = {
+ type: SystemCommentSubType.create;
+};
+
+export type TaskCompletedCommentTemplateVariables = {
+ type: SystemCommentSubType.complete;
+ taskId?: Task['id'];
+};
+
+export type TaskCommentTemplateVariables =
+ | TaskUpdateCommentTemplateVariables
+ | TaskCreateCommentTemplateVariables
+ | TaskCompletedCommentTemplateVariables;
+
+export type RepeatSchedule = Record & {
+ freq?: number;
+ interval?: number;
+ bymonthday?: number | number[] | null;
+ bysetpos?: number | number[] | null;
+ dtstart?: Date | null;
+};
diff --git a/packages/types/src/types/models.ts b/packages/types/src/types/models.ts
index ea87740d8a..c345cfaeb9 100644
--- a/packages/types/src/types/models.ts
+++ b/packages/types/src/types/models.ts
@@ -12,8 +12,10 @@ import { DashboardItemConfig } from './models-extra';
import { MapOverlayConfig } from './models-extra';
import { EntityAttributes } from './models-extra';
import { UserAccountPreferences } from './models-extra';
-import { ProjectConfig } from './models-extra';
import { EntityType } from './models-extra';
+import { ProjectConfig } from './models-extra';
+import { TaskCommentTemplateVariables } from './models-extra';
+import { RepeatSchedule } from './models-extra';
export interface AccessRequest {
'approved'?: boolean | null;
@@ -1536,29 +1538,73 @@ export interface SyncGroupLogUpdate {
}
export interface Task {
'assignee_id'?: string | null;
- 'due_date'?: Date | null;
+ 'created_at': Date;
+ 'due_date'?: number | null;
'entity_id': string;
'id': string;
- 'repeat_schedule'?: {} | null;
+ 'initial_request_id'?: string | null;
+ 'overdue_email_sent'?: Date | null;
+ 'parent_task_id'?: string | null;
+ 'repeat_schedule'?: RepeatSchedule | null;
'status'?: TaskStatus | null;
'survey_id': string;
+ 'survey_response_id'?: string | null;
}
export interface TaskCreate {
'assignee_id'?: string | null;
- 'due_date'?: Date | null;
+ 'created_at'?: Date;
+ 'due_date'?: number | null;
'entity_id': string;
- 'repeat_schedule'?: {} | null;
+ 'initial_request_id'?: string | null;
+ 'overdue_email_sent'?: Date | null;
+ 'parent_task_id'?: string | null;
+ 'repeat_schedule'?: RepeatSchedule | null;
'status'?: TaskStatus | null;
'survey_id': string;
+ 'survey_response_id'?: string | null;
}
export interface TaskUpdate {
'assignee_id'?: string | null;
- 'due_date'?: Date | null;
+ 'created_at'?: Date;
+ 'due_date'?: number | null;
'entity_id'?: string;
'id'?: string;
- 'repeat_schedule'?: {} | null;
+ 'initial_request_id'?: string | null;
+ 'overdue_email_sent'?: Date | null;
+ 'parent_task_id'?: string | null;
+ 'repeat_schedule'?: RepeatSchedule | null;
'status'?: TaskStatus | null;
'survey_id'?: string;
+ 'survey_response_id'?: string | null;
+}
+export interface TaskComment {
+ 'created_at': Date;
+ 'id': string;
+ 'message'?: string | null;
+ 'task_id': string;
+ 'template_variables': TaskCommentTemplateVariables;
+ 'type': TaskCommentType;
+ 'user_id'?: string | null;
+ 'user_name': string;
+}
+export interface TaskCommentCreate {
+ 'created_at'?: Date;
+ 'message'?: string | null;
+ 'task_id': string;
+ 'template_variables'?: TaskCommentTemplateVariables;
+ 'type'?: TaskCommentType;
+ 'user_id'?: string | null;
+ 'user_name': string;
+}
+export interface TaskCommentUpdate {
+ 'created_at'?: Date;
+ 'id'?: string;
+ 'message'?: string | null;
+ 'task_id'?: string;
+ 'template_variables'?: TaskCommentTemplateVariables;
+ 'type'?: TaskCommentType;
+ 'user_id'?: string | null;
+ 'user_name'?: string;
}
export interface TupaiaWebSession {
'access_policy': {};
@@ -1697,6 +1743,10 @@ export enum TaskStatus {
'cancelled' = 'cancelled',
'completed' = 'completed',
}
+export enum TaskCommentType {
+ 'user' = 'user',
+ 'system' = 'system',
+}
export enum SyncGroupSyncStatus {
'IDLE' = 'IDLE',
'SYNCING' = 'SYNCING',
@@ -1731,6 +1781,8 @@ export enum QuestionType {
'Radio' = 'Radio',
'SubmissionDate' = 'SubmissionDate',
'File' = 'File',
+ 'Task' = 'Task',
+ 'User' = 'User',
}
export enum PrimaryPlatform {
'tupaia' = 'tupaia',
diff --git a/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts b/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts
index 7cf45e43f9..2ac0f3fae1 100644
--- a/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts
+++ b/packages/types/src/types/requests/datatrak-web-server/EntityDescendantsRequest.ts
@@ -8,6 +8,7 @@ import { KeysToCamelCase } from '../../../utils/casing';
type EntityResponse = Entity & {
isRecent?: boolean;
+ parent_name?: Entity['name'];
};
export type Params = Record;
@@ -27,4 +28,5 @@ export type ReqQuery = {
type?: string;
};
searchString?: string;
+ pageSize?: number;
};
diff --git a/packages/types/src/types/requests/datatrak-web-server/SingleSurveyResponseRequest.ts b/packages/types/src/types/requests/datatrak-web-server/SingleSurveyResponseRequest.ts
index 270fafe450..b264782bdc 100644
--- a/packages/types/src/types/requests/datatrak-web-server/SingleSurveyResponseRequest.ts
+++ b/packages/types/src/types/requests/datatrak-web-server/SingleSurveyResponseRequest.ts
@@ -14,9 +14,12 @@ export interface ResBody extends KeysToCamelCase;
countryName: Country['name'];
entityName: Entity['name'];
+ entityId: Entity['id'];
surveyName: Survey['name'];
surveyCode: Survey['code'];
+ countryCode: Country['code'];
dataTime: Date;
+ entityParentName: Entity['name'];
}
export type ReqBody = Record;
export type ReqQuery = Record;
diff --git a/packages/types/src/types/requests/datatrak-web-server/SubmitSurveyResponseRequest.ts b/packages/types/src/types/requests/datatrak-web-server/SubmitSurveyResponseRequest.ts
index e2864ea7a8..689a3d293a 100644
--- a/packages/types/src/types/requests/datatrak-web-server/SubmitSurveyResponseRequest.ts
+++ b/packages/types/src/types/requests/datatrak-web-server/SubmitSurveyResponseRequest.ts
@@ -10,7 +10,12 @@ export type FileUploadAnswer = {
value: string;
};
-export type Answer = string | number | boolean | null | undefined | FileUploadAnswer;
+export type UserAnswer = {
+ id: string;
+ name: string;
+};
+
+export type Answer = string | number | boolean | null | undefined | FileUploadAnswer | UserAnswer;
export type Answers = Record;
diff --git a/packages/types/src/types/requests/datatrak-web-server/SurveyRequest.ts b/packages/types/src/types/requests/datatrak-web-server/SurveyRequest.ts
index 930c703d74..04589a411a 100644
--- a/packages/types/src/types/requests/datatrak-web-server/SurveyRequest.ts
+++ b/packages/types/src/types/requests/datatrak-web-server/SurveyRequest.ts
@@ -56,6 +56,7 @@ export type SurveyScreenComponent = CamelCasedComponent &
label?: BaseSurveyScreenComponent['question_label'];
options?: Option[] | null;
screenId?: string;
+ id?: string;
};
type CamelCasedSurveyScreen = KeysToCamelCase>;
@@ -77,4 +78,5 @@ export type ReqBody = Record;
export interface ReqQuery {
fields?: string[];
projectId?: string;
+ countryCode?: string;
}
diff --git a/packages/types/src/types/requests/datatrak-web-server/TaskChangeRequest.ts b/packages/types/src/types/requests/datatrak-web-server/TaskChangeRequest.ts
new file mode 100644
index 0000000000..9df6643d4e
--- /dev/null
+++ b/packages/types/src/types/requests/datatrak-web-server/TaskChangeRequest.ts
@@ -0,0 +1,22 @@
+/*
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import { Survey, Task } from '../../models';
+import { RepeatSchedule } from '../../models-extra';
+
+export type Params = Record;
+export type ResBody = {
+ message: string;
+};
+export type ReqQuery = Record;
+export type ReqBody = Partial> & {
+ survey_code: Survey['code'];
+ comment?: string;
+ repeat_frequency?: RepeatSchedule['freq'];
+ assignee?: {
+ value: string;
+ label: string;
+ } | null;
+};
diff --git a/packages/types/src/types/requests/datatrak-web-server/TaskMetricsRequest.ts b/packages/types/src/types/requests/datatrak-web-server/TaskMetricsRequest.ts
new file mode 100644
index 0000000000..b97cc48686
--- /dev/null
+++ b/packages/types/src/types/requests/datatrak-web-server/TaskMetricsRequest.ts
@@ -0,0 +1,8 @@
+export type Params = Record;
+export interface ResBody {
+ unassignedTasks: number;
+ overdueTasks: number;
+ onTimeCompletionRate: number;
+}
+export type ReqBody = Record;
+export type ReqQuery = Record;
diff --git a/packages/types/src/types/requests/datatrak-web-server/TaskRequest.ts b/packages/types/src/types/requests/datatrak-web-server/TaskRequest.ts
new file mode 100644
index 0000000000..27fefdc854
--- /dev/null
+++ b/packages/types/src/types/requests/datatrak-web-server/TaskRequest.ts
@@ -0,0 +1,22 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import { KeysToCamelCase } from '../../../utils';
+import { TaskComment } from '../../models';
+import { TaskResponse } from './TasksRequest';
+
+export type Params = {
+ taskId: string;
+};
+
+type Comment = Omit, 'createdAt'> & {
+ // handle the fact that KeysToCamelCase changes Date keys to to camelCase as well
+ createdAt: Date;
+};
+export type ResBody = TaskResponse & {
+ comments: Comment[];
+};
+export type ReqBody = Record;
+export type ReqQuery = Record;
diff --git a/packages/types/src/types/requests/datatrak-web-server/TasksRequest.ts b/packages/types/src/types/requests/datatrak-web-server/TasksRequest.ts
new file mode 100644
index 0000000000..5b29ac579c
--- /dev/null
+++ b/packages/types/src/types/requests/datatrak-web-server/TasksRequest.ts
@@ -0,0 +1,51 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import { KeysToCamelCase } from '../../../utils/casing';
+import { Entity, Survey, Task, TaskStatus } from '../../models';
+
+export type Params = Record;
+
+type Assignee = {
+ id?: string | null;
+ name?: string | null;
+};
+
+export type TaskResponse = KeysToCamelCase<
+ Partial>
+> & {
+ assignee?: Assignee;
+ taskStatus: TaskStatus | 'overdue' | 'repeating';
+ survey: {
+ name: Survey['name'];
+ id: Survey['id'];
+ code: Survey['code'];
+ };
+ entity: {
+ name: Entity['name'];
+ id: Entity['id'];
+ code: Entity['code'];
+ countryCode: string; // this is not undefined or null so use string explicitly here
+ parentName?: Entity['name'];
+ };
+ repeatSchedule?: Record | null;
+ taskDueDate?: Date | null;
+};
+
+export type ResBody = {
+ tasks: (TaskResponse & {
+ commentsCount: number;
+ })[];
+ count: number;
+ numberOfPages: number;
+};
+export type ReqBody = Record;
+export interface ReqQuery {
+ fields?: string[];
+ pageSize?: number;
+ sort?: string[];
+ page?: number;
+ filters?: Record[];
+}
diff --git a/packages/types/src/types/requests/datatrak-web-server/UserRequest.ts b/packages/types/src/types/requests/datatrak-web-server/UserRequest.ts
index 6e9320e5e1..87699714b8 100644
--- a/packages/types/src/types/requests/datatrak-web-server/UserRequest.ts
+++ b/packages/types/src/types/requests/datatrak-web-server/UserRequest.ts
@@ -9,7 +9,7 @@ import { Country } from '../../models';
export type Params = Record;
export interface ResBody {
id?: string;
- userName?: string;
+ fullName?: string;
firstName?: string;
lastName?: string;
email?: string;
diff --git a/packages/types/src/types/requests/datatrak-web-server/UsersRequest.ts b/packages/types/src/types/requests/datatrak-web-server/UsersRequest.ts
new file mode 100644
index 0000000000..e2770b648a
--- /dev/null
+++ b/packages/types/src/types/requests/datatrak-web-server/UsersRequest.ts
@@ -0,0 +1,20 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import { PermissionGroup } from '../../models';
+
+export type Params = Record;
+
+type UserResponse = {
+ id: string;
+ name: string;
+};
+
+export type ResBody = UserResponse[];
+export type ReqBody = Record;
+export interface ReqQuery {
+ searchTerm?: string;
+ permissionGroupId?: PermissionGroup['id'];
+}
diff --git a/packages/types/src/types/requests/datatrak-web-server/index.ts b/packages/types/src/types/requests/datatrak-web-server/index.ts
index fcec54e1bc..598904707c 100644
--- a/packages/types/src/types/requests/datatrak-web-server/index.ts
+++ b/packages/types/src/types/requests/datatrak-web-server/index.ts
@@ -17,3 +17,8 @@ export * as DatatrakWebLeaderboardRequest from './LeaderboardRequest';
export * as DatatrakWebActivityFeedRequest from './ActivityFeedRequest';
export * as DatatrakWebGenerateLoginTokenRequest from './GenerateLoginTokenRequest';
export * as DatatrakWebEntityDescendantsRequest from './EntityDescendantsRequest';
+export * as DatatrakWebTaskMetricsRequest from './TaskMetricsRequest';
+export * as DatatrakWebTasksRequest from './TasksRequest';
+export * as DatatrakWebTaskRequest from './TaskRequest';
+export * as DatatrakWebUsersRequest from './UsersRequest';
+export * as DatatrakWebTaskChangeRequest from './TaskChangeRequest';
diff --git a/packages/types/src/types/requests/index.ts b/packages/types/src/types/requests/index.ts
index d3010441b9..b93544c385 100644
--- a/packages/types/src/types/requests/index.ts
+++ b/packages/types/src/types/requests/index.ts
@@ -20,6 +20,11 @@ export {
DatatrakWebActivityFeedRequest,
DatatrakWebGenerateLoginTokenRequest,
DatatrakWebEntityDescendantsRequest,
+ DatatrakWebTaskMetricsRequest,
+ DatatrakWebTasksRequest,
+ DatatrakWebTaskRequest,
+ DatatrakWebUsersRequest,
+ DatatrakWebTaskChangeRequest,
} from './datatrak-web-server';
export {
TupaiaWebChangePasswordRequest,
diff --git a/packages/ui-components/src/components/ActionsMenu.tsx b/packages/ui-components/src/components/ActionsMenu.tsx
index 2d0724ec7e..4291beba15 100644
--- a/packages/ui-components/src/components/ActionsMenu.tsx
+++ b/packages/ui-components/src/components/ActionsMenu.tsx
@@ -5,20 +5,30 @@
import React from 'react';
import {
+ IconButton as MuiIconButton,
ListItemIcon,
- MenuItem as MuiMenuItem,
Menu as MuiMenu,
- IconButton,
- Typography,
+ MenuItem as MuiMenuItem,
Tooltip,
+ Typography,
} from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import styled from 'styled-components';
import { ActionsMenuOptionType } from '../types';
+const StyledMenu = styled(MuiMenu)`
+ .MuiPaper-root {
+ border: 1px solid ${props => props.theme.palette.divider};
+ }
+ .MuiList-root {
+ padding: 0.2rem;
+ }
+`;
+
const StyledMenuItem = styled(MuiMenuItem)`
- padding-top: 0.625rem;
- padding-bottom: 0.625rem;
+ padding: 0.3rem 0.3rem;
+ font-size: 0.75rem;
+ min-width: 5rem;
`;
const StyledMenuIcon = styled(MoreVertIcon)`
@@ -40,6 +50,7 @@ type ActionMenuProps = {
vertical?: 'top' | 'bottom';
horizontal?: 'left' | 'right';
};
+ IconButton?: typeof MuiIconButton;
};
export const ActionsMenu = ({
@@ -47,14 +58,15 @@ export const ActionsMenu = ({
includesIcons = false,
anchorOrigin = {},
transformOrigin = {},
+ IconButton = MuiIconButton,
}: ActionMenuProps) => {
const [anchorEl, setAnchorEl] = React.useState<(EventTarget & HTMLButtonElement) | null>(null);
return (
<>
- setAnchorEl(event.currentTarget)}>
+ setAnchorEl(event.currentTarget)}>
- setAnchorEl(null)}
anchorOrigin={{
- vertical: 'bottom',
- horizontal: 'left',
+ vertical: 'top',
+ horizontal: 'right',
...anchorOrigin,
}}
transformOrigin={{ horizontal: 'right', vertical: 'top', ...transformOrigin }}
@@ -104,7 +116,7 @@ export const ActionsMenu = ({
),
)}
-
+
>
);
};
diff --git a/packages/ui-components/src/components/Alert.tsx b/packages/ui-components/src/components/Alert.tsx
index 6ace97a69f..b7f8b2006b 100644
--- a/packages/ui-components/src/components/Alert.tsx
+++ b/packages/ui-components/src/components/Alert.tsx
@@ -56,6 +56,7 @@ const StyledSmallAlert = styled(StyledAlert)`
padding-block: 0;
padding-inline: 1rem;
box-shadow: none;
+ word-break: break-word;
.MuiAlert-icon {
padding: 0.5rem 0;
diff --git a/packages/ui-components/src/components/Button.tsx b/packages/ui-components/src/components/Button.tsx
index 3bcbc8119f..43179a2cf2 100644
--- a/packages/ui-components/src/components/Button.tsx
+++ b/packages/ui-components/src/components/Button.tsx
@@ -11,7 +11,7 @@ import { OverrideableComponentProps } from '../types';
const StyledButton = styled(MuiButton)`
line-height: 1.75;
letter-spacing: 0;
- padding: 0.5em 1.75em;
+ padding: 0.5rem 1.75rem;
box-shadow: none;
min-width: 3rem;
diff --git a/packages/ui-components/src/components/FilterableTable/Cells.tsx b/packages/ui-components/src/components/FilterableTable/Cells.tsx
index a8c442048b..37df653e17 100644
--- a/packages/ui-components/src/components/FilterableTable/Cells.tsx
+++ b/packages/ui-components/src/components/FilterableTable/Cells.tsx
@@ -32,7 +32,7 @@ const CellContentWrapper = styled.div`
align-items: center;
tr:not(:last-child) & {
- border-bottom: 1px solid ${({ theme }) => theme.palette.grey[400]};
+ border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
}
td:first-child & {
padding-inline-start: 0.2rem;
@@ -53,7 +53,7 @@ const HeaderCell = styled(Cell)`
color: ${({ theme }) => theme.palette.text.secondary};
font-weight: ${({ theme }) => theme.typography.fontWeightMedium};
background-color: ${({ theme }) => theme.palette.background.paper};
- border-bottom: 1px solid ${({ theme }) => theme.palette.grey[400]};
+ border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
padding-block: 0.7rem;
padding-inline: 0.7rem 0;
display: flex;
@@ -71,7 +71,7 @@ const CellLink = styled(Link)`
color: inherit;
text-decoration: none;
&:hover {
- tr:has(&) td > * {
+ tr:has(&) td {
background-color: ${({ theme }) => `${theme.palette.primary.main}18`}; // 18 is 10% opacity
}
}
@@ -116,15 +116,20 @@ interface TableCellProps {
width?: string;
row: Record;
maxWidth?: number;
+ column?: Record;
}
-export const TableCell = ({ children, width, row, maxWidth, ...props }: TableCellProps) => {
- const url = row?.original?.url;
+export const TableCell = ({ children, width, row, maxWidth, column, ...props }: TableCellProps) => {
+ const getRowUrl = () => {
+ if (!row) return {};
+ if (row.url) return { to: row.url };
+ if (column?.generateUrl) return column.generateUrl(row);
+ return {};
+ };
+ const { to, state } = getRowUrl();
return (
-
-
- {children}
-
+
+ {children}
|
);
diff --git a/packages/ui-components/src/components/FilterableTable/FilterCell.tsx b/packages/ui-components/src/components/FilterableTable/FilterCell.tsx
index d939bedd44..0524f22f7d 100644
--- a/packages/ui-components/src/components/FilterableTable/FilterCell.tsx
+++ b/packages/ui-components/src/components/FilterableTable/FilterCell.tsx
@@ -2,17 +2,22 @@
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
-import { HeaderDisplayCell, HeaderDisplayCellProps } from './Cells';
-import { TextField } from '../Inputs';
import { Search } from '@material-ui/icons';
+import { StandardTextFieldProps } from '@material-ui/core';
import { ColumnInstance } from 'react-table';
+import { TextField } from '../Inputs';
+import { HeaderDisplayCell, HeaderDisplayCellProps } from './Cells';
+import { useDebounce } from '../../hooks';
const FilterWrapper = styled.div`
.MuiFormControl-root {
margin-block-end: 0;
}
+ .MuiOutlinedInput-notchedOutline {
+ border-color: ${({ theme }) => theme.palette.divider};
+ }
.MuiInputBase-input,
.MuiOutlinedInput-root {
font-size: inherit;
@@ -39,15 +44,12 @@ const FilterWrapper = styled.div`
.MuiAutocomplete-option {
padding-block: 0.5rem;
}
+ .MuiInputBase-input::-webkit-input-placeholder {
+ color: ${({ theme }) => theme.palette.text.secondary};
+ }
`;
-export const DefaultFilter = styled(TextField).attrs(props => ({
- InputProps: {
- ...props.InputProps,
- startAdornment: ,
- },
- placeholder: 'Search...',
-}))`
+const DefaultFilterInput = styled(TextField)`
margin-block-end: 0;
font-size: inherit;
width: 100%;
@@ -64,10 +66,41 @@ export const DefaultFilter = styled(TextField).attrs(props => ({
padding-inline-start: 0.3rem;
}
.MuiSvgIcon-root {
- color: ${({ theme }) => theme.palette.text.tertiary};
+ color: ${({ theme }) => theme.palette.divider};
}
`;
+interface DefaultFilterProps extends Omit {
+ value?: string | null;
+ onChange: (value: string) => void;
+}
+
+const DefaultFilter = ({ value, onChange, ...props }: DefaultFilterProps) => {
+ const [stateValue, setStateValue] = useState(value ?? '');
+ const debouncedSearchValue = useDebounce(stateValue, 500);
+
+ useEffect(() => {
+ if (debouncedSearchValue === value) return;
+ onChange(debouncedSearchValue);
+ }, [debouncedSearchValue]);
+
+ useEffect(() => {
+ if (value === stateValue) return;
+ setStateValue(value ?? '');
+ }, [value]);
+ return (
+ setStateValue(e.target.value)}
+ InputProps={{
+ startAdornment: ,
+ }}
+ placeholder="Search..."
+ />
+ );
+};
+
export type Filters = Record[];
export interface FilterCellProps extends Partial {
@@ -76,6 +109,7 @@ export interface FilterCellProps extends Partial {
column: ColumnInstance>;
filter?: any;
onChange: (value: any) => void;
+ value: any;
}>;
filterable?: boolean;
};
@@ -99,11 +133,16 @@ export const FilterCell = ({ column, filters, onChangeFilters, ...props }: Filte
{Filter ? (
-
+
) : (
handleUpdate(e.target.value)}
+ onChange={handleUpdate}
aria-label={`Search ${column.Header}`}
/>
)}
diff --git a/packages/ui-components/src/components/FilterableTable/FilterableTable.tsx b/packages/ui-components/src/components/FilterableTable/FilterableTable.tsx
index 7c2ceae9d0..d263838d9c 100644
--- a/packages/ui-components/src/components/FilterableTable/FilterableTable.tsx
+++ b/packages/ui-components/src/components/FilterableTable/FilterableTable.tsx
@@ -11,10 +11,12 @@ import {
TableHead,
TableRow,
TableSortLabel,
+ Typography,
} from '@material-ui/core';
import styled from 'styled-components';
import { Column, useFlexLayout, useResizeColumns, useTable, SortingRule } from 'react-table';
import { KeyboardArrowDown } from '@material-ui/icons';
+import { SpinningLoader } from '../Loaders';
import { HeaderDisplayCell, TableCell } from './Cells';
import { FilterCell, FilterCellProps, Filters } from './FilterCell';
import { Pagination } from './Pagination';
@@ -23,6 +25,9 @@ const TableContainer = styled(MuiTableContainer)`
position: relative;
flex: 1;
overflow: auto;
+ display: flex;
+ flex-direction: column;
+ background-color: ${({ theme }) => theme.palette.background.paper};
table {
min-width: 45rem;
}
@@ -31,23 +36,39 @@ const TableContainer = styled(MuiTableContainer)`
position: sticky;
top: 0;
z-index: 2;
- background-color: ${({ theme }) => theme.palette.background.paper};
}
tr {
display: flex;
-
+ }
.MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline {
border-color: ${({ theme }) => theme.palette.primary.main};
}
`;
+const NoDataMessage = styled.div`
+ width: 100%;
+ text-align: center;
+ padding-block: 2.5rem;
+`;
+
+const LoadingContainer = styled.div`
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
type SortBy = {
id: string;
desc: boolean;
};
+type ColumnInstance = Record & {
+ CellContentComponent?: React.ComponentType;
+};
+
interface FilterableTableProps {
- columns: Column>[];
+ columns: Column[];
data?: Record[];
pageIndex?: number;
pageSize?: number;
@@ -57,12 +78,11 @@ interface FilterableTableProps {
onChangePage: (pageIndex: number) => void;
onChangePageSize: (pageSize: number) => void;
onChangeSorting: (sorting: SortingRule>[]) => void;
- refreshData: () => void;
- isLoading: boolean;
- errorMessage: string;
onChangeFilters: FilterCellProps['onChangeFilters'];
filters?: Filters;
totalRecords: number;
+ noDataMessage?: string;
+ isLoading?: boolean;
}
export const FilterableTable = ({
@@ -79,6 +99,8 @@ export const FilterableTable = ({
onChangeFilters,
filters = [],
totalRecords,
+ noDataMessage,
+ isLoading,
}: FilterableTableProps) => {
const memoisedData = useMemo(() => data ?? [], [data]);
const {
@@ -189,13 +211,14 @@ export const FilterableTable = ({
return (
{row.cells.map(({ getCellProps, render }, i) => {
- const col = visibleColumns[i];
+ const col = visibleColumns[i] as ColumnInstance;
return (
{render('Cell')}
@@ -206,6 +229,16 @@ export const FilterableTable = ({
})}
+ {rows.length === 0 && noDataMessage && !isLoading && (
+
+ {noDataMessage}
+
+ )}
+ {isLoading && (
+
+
+
+ )}
theme.palette.grey['400']};
+ border-top: 1px solid ${({ theme }) => theme.palette.divider};
background-color: ${({ theme }) => theme.palette.background.paper};
}
.MuiSelect-root {
padding-block: 0.5rem;
padding-inline: 0.8rem 0.2rem;
}
+ .MuiOutlinedInput-notchedOutline,
+ .MuiInput-root,
+ .MuiButtonBase-root {
+ border-color: ${({ theme }) => theme.palette.divider};
+ }
`;
interface PaginationProps {
@@ -34,17 +39,16 @@ export const Pagination = ({
onChangePageSize,
totalRecords,
}: PaginationProps) => {
- if (!totalRecords) return null;
-
return (
);
diff --git a/packages/ui-components/src/components/Inputs/Autocomplete.tsx b/packages/ui-components/src/components/Inputs/Autocomplete.tsx
index 3850b23224..423b6536a0 100644
--- a/packages/ui-components/src/components/Inputs/Autocomplete.tsx
+++ b/packages/ui-components/src/components/Inputs/Autocomplete.tsx
@@ -116,6 +116,7 @@ export const Autocomplete = ({
renderOption={renderOption}
popupIcon={}
PaperComponent={StyledPaper}
+ blurOnSelect
renderInput={params => (
{
- const [status, setStatus] = useState(STATUS.IDLE);
- const [errorMessage, setErrorMessage] = useState(null);
- const [successMessage, setSuccessMessage] = useState(null);
- const [file, setFile] = useState(null);
-
- const handleSubmit = async event => {
- event.preventDefault();
- setErrorMessage(null);
- setStatus(STATUS.LOADING);
-
- try {
- const { message } = await onSubmit(file);
- if (showLoadingContainer && message) {
- setStatus(STATUS.SUCCESS);
- setSuccessMessage(message);
- } else {
- handleClose();
- }
- } catch (error) {
- setStatus(STATUS.ERROR);
- setErrorMessage(error.message);
- }
- };
-
- const handleClose = async () => {
- onClose();
-
- setStatus(STATUS.IDLE);
- setErrorMessage(null);
- setSuccessMessage(null);
- setFile(null);
- };
-
- const handleDismiss = () => {
- setStatus(STATUS.IDLE);
- setErrorMessage(null);
- setSuccessMessage(null);
- // Deselect file when dismissing an error, this avoids an error when editing selected files
- // @see https://github.com/beyondessential/tupaia-backlog/issues/1211
- setFile(null);
- };
-
- const ContentContainer = showLoadingContainer
- ? ({ children }) => (
-
- {children}
-
- )
- : React.Fragment;
-
- const renderContent = useCallback(() => {
- switch (status) {
- case STATUS.SUCCESS:
- return {successMessage}
;
- case STATUS.ERROR:
- return (
- <>
- An error has occurred.
-
- {errorMessage}
-
- >
- );
- default:
- return (
- <>
- {subtitle}
-
- >
- );
- }
- }, [status, successMessage, errorMessage, subtitle]);
-
- const renderButtons = useCallback(() => {
- switch (status) {
- case STATUS.SUCCESS:
- return ;
- case STATUS.ERROR:
- return (
- <>
- Dismiss
-
- >
- );
- default:
- return (
- <>
-
-
- >
- );
- }
- }, [status, file, handleDismiss, handleClose, handleSubmit]);
-
- return (
-
- );
-};
-
-ImportModal.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- title: PropTypes.string,
- subtitle: PropTypes.string,
- actionText: PropTypes.string,
- loadingText: PropTypes.string,
- loadingHeading: PropTypes.string,
- showLoadingContainer: PropTypes.bool,
- onSubmit: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired,
-};
-
-ImportModal.defaultProps = {
- title: 'Import',
- subtitle: '',
- actionText: 'Import',
- loadingText: 'Importing',
- loadingHeading: 'Importing data',
- showLoadingContainer: false,
-};
diff --git a/packages/admin-panel/src/widgets/Modal/Modal.jsx b/packages/ui-components/src/components/Modal/Modal.tsx
similarity index 64%
rename from packages/admin-panel/src/widgets/Modal/Modal.jsx
rename to packages/ui-components/src/components/Modal/Modal.tsx
index 9a0eed83e2..a0774d4e9d 100644
--- a/packages/admin-panel/src/widgets/Modal/Modal.jsx
+++ b/packages/ui-components/src/components/Modal/Modal.tsx
@@ -4,10 +4,10 @@
*/
import React from 'react';
import styled from 'styled-components';
-import PropTypes from 'prop-types';
-import { DialogFooter as BaseDialogFooter, Button } from '@tupaia/ui-components';
-import { Dialog } from '@material-ui/core';
-import { ModalContentProvider } from './ModalContentProvider';
+import { ButtonProps, Dialog, DialogProps } from '@material-ui/core';
+import { DialogFooter as BaseDialogFooter } from '../Dialog';
+import { Button } from '../Button';
+import { ModalContentProvider, ModalContentProviderProps } from './ModalContentProvider';
import { ModalHeader } from './ModalHeader';
export const ModalFooter = styled(BaseDialogFooter)`
@@ -16,6 +16,25 @@ export const ModalFooter = styled(BaseDialogFooter)`
padding-inline: 1.9rem;
`;
+type ButtonT = Omit & {
+ id: string;
+ text: string;
+ component?: React.ElementType;
+ to?: string;
+ type?: string;
+ variant?: string; // declare as a string here because passing 'contained' or 'outlined' is coming up as invalid elsewhere
+};
+
+interface ModalProps extends Omit {
+ children: React.ReactNode;
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ isLoading?: boolean;
+ error?: ModalContentProviderProps['error'];
+ buttons?: ButtonT[];
+}
+
export const Modal = ({
children,
isOpen,
@@ -23,9 +42,9 @@ export const Modal = ({
title,
isLoading,
error,
- buttons,
+ buttons = [],
...muiDialogProps
-}) => {
+}: ModalProps) => {
const getModalTitle = () => {
if (error) {
return title || 'Error';
@@ -55,6 +74,7 @@ export const Modal = ({
to,
}) => (