Skip to content

Commit

Permalink
Merge pull request #16 from paritytech/chore/fixes
Browse files Browse the repository at this point in the history
Chore/fixes
  • Loading branch information
ba1uev authored Mar 7, 2024
2 parents 977ff28 + 0c446e9 commit efcfd58
Show file tree
Hide file tree
Showing 20 changed files with 248 additions and 87 deletions.
26 changes: 22 additions & 4 deletions src/client/components/ui/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React, { useEffect, useState } from 'react'
import { cn } from '#client/utils'
import { Button } from './Button'
const DISPLAY_DURATION = 6000
const EventKey = 'phq_notification'

type NotificationKind = 'success' | 'warning' | 'error' | 'info'
type Notification = {
kind: NotificationKind
text: string
kind: NotificationKind
link?: { text: string; url: string }
}

export function showNotification(
text: string,
kind: NotificationKind = 'info'
text: Notification['text'],
kind: Notification['kind'] = 'info',
link?: Notification['link']
): void {
const notification: Notification = { text, kind }
const notification: Notification = { text, kind, link }
const event = new CustomEvent(EventKey, { detail: notification })
window.dispatchEvent(event)
}
Expand Down Expand Up @@ -68,6 +71,21 @@ export const Notifications: React.FC = () => {
)}
>
{notification.text}
{!!notification.link && (
<>
{' '}
<a
className={cn(
getStylingClassNames(notification.kind),
'underline hover:underline'
)}
href={notification.link.url}
rel="external"
>
{notification.link.text}
</a>
</>
)}
</div>
)}
</div>
Expand Down
1 change: 0 additions & 1 deletion src/client/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ td:last-child {
z-index: 40;
}
.phq_notifications-item {
display: flex;
box-sizing: border-box;
position: absolute;
right: 0;
Expand Down
28 changes: 20 additions & 8 deletions src/client/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@ export const api = axios.create({
withCredentials: true,
})

api.interceptors.response.use((res) => res, (err: AxiosError<{ statusCode: number; message: string }>) => {
if (err.response?.data?.statusCode !== 401) {
if (err.code === 'ERR_NETWORK') {
showNotification('No internet connection.', 'warning')
api.interceptors.response.use(
(res) => res,
(err: AxiosError<{ statusCode: number; message: string }>) => {
if (err.response?.status === 401) {
setTimeout(() => {
if (!['/login', '/polkadot'].includes(window.location.pathname)) {
showNotification('Your session has expired.', 'info', {
text: 'Login',
url: '/auth/logout',
})
}
}, 1e3)
} else {
const message = err.response?.data?.message || 'Something went wrong.'
showNotification(message, 'error')
if (err.code === 'ERR_NETWORK') {
showNotification('No internet connection.', 'warning')
} else {
const message = err.response?.data?.message || 'Something went wrong.'
showNotification(message, 'error')
}
}
throw err
}
throw err
})
)
7 changes: 7 additions & 0 deletions src/modules/events/client/components/AdminEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,13 @@ export const AdminEvents = () => {
</Link>
),
},
{
Header: 'Data retention',
accessor: (event: EventAdminResponse) =>
event.purgeSubmissionsAfterDays
? `${event.purgeSubmissionsAfterDays} days`
: 'Forever',
},
{
Header: 'Form editor',
accessor: (event: EventAdminResponse) =>
Expand Down
7 changes: 0 additions & 7 deletions src/modules/events/server/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,6 @@ export class Event
}
}

useAdminView(applicationsCount: number): EventAdminResponse {
return {
...this.toJSON(),
applicationsCount,
}
}

usePreviewView(): EventPreview {
return {
id: this.id,
Expand Down
19 changes: 15 additions & 4 deletions src/modules/events/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,13 +836,24 @@ const adminRouter: FastifyPluginCallback = async function (fastify, opts) {
where,
order: [['startDate', 'DESC']],
})
const formIds = Array.from(
new Set(events.map(fp.prop('formId')).filter(Boolean))
) as string[]
const forms = await fastify.db.Form.findAll({
where: { id: { [Op.in]: formIds } },
})
const formById = forms.reduce(fp.by('id'), {})

const applicationsCountByEventId =
await fastify.db.EventApplication.countByEventId()
const result: EventAdminResponse[] = events.map((x) => ({
...x.toJSON(),
applicationsCount: applicationsCountByEventId[x.id] || 0,
}))
const result: EventAdminResponse[] = events.map((x) => {
const form = x.formId ? formById[x.formId] : null
return {
...x.toJSON(),
applicationsCount: applicationsCountByEventId[x.id] || 0,
purgeSubmissionsAfterDays: form?.purgeSubmissionsAfterDays ?? null,
}
})
return result
}
)
Expand Down
1 change: 1 addition & 0 deletions src/modules/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type EventExternalIds = {

export interface EventAdminResponse extends Event {
applicationsCount: number
purgeSubmissionsAfterDays: number | null
}

export interface EventPublicResponse extends Event {
Expand Down
40 changes: 39 additions & 1 deletion src/modules/forms/client/components/AdminFormEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
CheckboxGroup,
Breadcrumbs,
LabelWrapper,
Select,
} from '#client/components/ui'
import { showNotification } from '#client/components/ui/Notifications'
import { EntityAccessSelector } from '#client/components/EntityAccessSelector'
Expand All @@ -37,8 +38,12 @@ import {
const EmptyFormContent: FormStep[] = []
const DefaultFormContent: FormStep[] = getPlaceholderFormContent()

type FormFormData = Omit<FormCreationRequest, 'content'> & {
type FormFormData = Omit<
FormCreationRequest,
'content' | 'purgeSubmissionsAfterDays'
> & {
contentHash?: number
purgeSubmissionsAfterDays: string
}

export const AdminFormEditor: React.FC = () => {
Expand Down Expand Up @@ -85,6 +90,9 @@ export const AdminFormEditor: React.FC = () => {
responsibleUserIds: form.responsibleUserIds,
visibility: form.visibility,
allowedRoles: form.allowedRoles,
purgeSubmissionsAfterDays: String(
form.purgeSubmissionsAfterDays ?? 'none'
),
}))
if (form.responsibleUserIds?.length) {
setShowUsersList(true)
Expand Down Expand Up @@ -112,6 +120,7 @@ export const AdminFormEditor: React.FC = () => {
contentHash: 0,
visibility: EntityVisibility.None,
allowedRoles: [],
purgeSubmissionsAfterDays: 'none',
}
}, [])
const [formData, setFormData] = useState<FormFormData>(formInitialValue)
Expand Down Expand Up @@ -226,8 +235,22 @@ export const AdminFormEditor: React.FC = () => {
const contentFallback = form ? form.content : []
const formRequestData: FormCreationRequest = {
...values,
purgeSubmissionsAfterDays:
values.purgeSubmissionsAfterDays === 'none'
? null
: Number(values.purgeSubmissionsAfterDays),
content: formContentChanged || contentFallback,
}
if (
form &&
formRequestData.purgeSubmissionsAfterDays &&
!form.purgeSubmissionsAfterDays &&
!window.confirm(
`You are about to activate the auto-delete feature for all form submissions older than ${formRequestData.purgeSubmissionsAfterDays} days. This action will take effect soon and could impact previously collected submissions. Are you sure?`
)
) {
return
}
return form ? updateForm(formRequestData) : createForm(formRequestData)
},
[form, formContentChanged, formData]
Expand Down Expand Up @@ -388,6 +411,21 @@ export const AdminFormEditor: React.FC = () => {
</LabelWrapper>
</div>

<div className="my-6">
<LabelWrapper className="my-6 mt-2" label="Data retention">
<Select
options={[
{ value: 'none', label: 'Keep forever' },
{ value: '30', label: 'Delete after 30 days' },
{ value: '90', label: 'Delete after 90 days' },
{ value: '365', label: 'Delete after 365 days' },
]}
value={formData.purgeSubmissionsAfterDays || 'none'}
onChange={onFormChange('purgeSubmissionsAfterDays')}
/>
</LabelWrapper>
</div>

<div className="my-6">
<b className="block mb-6">Content</b>
<FormBuilder
Expand Down
7 changes: 7 additions & 0 deletions src/modules/forms/client/components/AdminForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export const AdminForms = () => {
</Link>
),
},
{
Header: 'Data retention',
accessor: (form: FormAdminResponse) =>
form.purgeSubmissionsAfterDays
? `${form.purgeSubmissionsAfterDays} days`
: 'Forever',
},
// {
// Header: 'Parent event',
// accessor: (form: FormAdminResponse) =>
Expand Down
2 changes: 1 addition & 1 deletion src/modules/forms/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"dependencies": ["users"],
"requiredIntegrations": [],
"recommendedIntegrations": ["email-smtp", "matrix"],
"availableCronJobs": ["forms-delete-data"],
"availableCronJobs": ["forms-delete-data", "purge-form-submissions"],
"models": ["Form", "FormSubmission"],
"clientRouter": {
"public": {
Expand Down
3 changes: 2 additions & 1 deletion src/modules/forms/server/jobs/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CronJob, CronJobContext } from '#server/types'
import * as formsDeleteDate from './forms-delete-data'
import * as purgeFormSubmissions from './purge-form-submissions'

module.exports.moduleCronJobsFactory = (ctx: CronJobContext): CronJob[] => {
return [formsDeleteDate.jobFactory()]
return [formsDeleteDate.jobFactory(), purgeFormSubmissions.jobFactory()]
}
38 changes: 38 additions & 0 deletions src/modules/forms/server/jobs/purge-form-submissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Op } from 'sequelize'
import dayjs from 'dayjs'
import { CronJob, CronJobContext } from '#server/types'

const JOB_NAME = 'purge-form-submissions'

export const jobFactory = (): CronJob => {
return {
name: JOB_NAME,
cron: `0 0 * * *`,
fn: async (ctx: CronJobContext) => {
const forms = await ctx.models.Form.findAll({
where: {
purgeSubmissionsAfterDays: { [Op.ne]: null },
},
attributes: ['id', 'title', 'purgeSubmissionsAfterDays'],
})
for (const form of forms) {
if (!form.purgeSubmissionsAfterDays) continue
const deleted = await ctx.models.FormSubmission.destroy({
where: {
formId: form.id,
createdAt: {
[Op.lt]: dayjs()
.subtract(form.purgeSubmissionsAfterDays, 'day')
.toDate(),
},
},
})
if (deleted) {
ctx.log.info(
`${JOB_NAME}: Deleted ${deleted} submissions. Form "${form.title}"`
)
}
}
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// @ts-check
const { Sequelize, DataTypes } = require('sequelize')

module.exports = {
async up({ context: queryInterface, appConfig }) {
await queryInterface.addColumn('forms', 'purgeSubmissionsAfterDays', {
type: DataTypes.INTEGER,
})
},
async down({ context: queryInterface, appConfig }) {
await queryInterface.removeColumn('forms', 'purgeSubmissionsAfterDays')
},
}
2 changes: 2 additions & 0 deletions src/modules/forms/server/models/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class Form
declare metadataFields: FormModel['metadataFields']
declare creatorUserId: FormModel['creatorUserId']
declare responsibleUserIds: FormModel['responsibleUserIds']
declare purgeSubmissionsAfterDays: FormModel['purgeSubmissionsAfterDays']
declare createdAt: CreationOptional<Date>
declare updatedAt: CreationOptional<Date>

Expand Down Expand Up @@ -123,6 +124,7 @@ Form.init(
type: DataTypes.ARRAY(DataTypes.UUID),
defaultValue: [],
},
purgeSubmissionsAfterDays: DataTypes.INTEGER,
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
},
Expand Down
3 changes: 2 additions & 1 deletion src/modules/forms/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface Form {
metadataFields: FormMetadataField[]
creatorUserId: string
responsibleUserIds: string[]

purgeSubmissionsAfterDays: number | null
createdAt: Date
updatedAt: Date
}
Expand Down Expand Up @@ -128,6 +128,7 @@ export type FormCreationRequest = Pick<
| 'content'
| 'metadataFields'
| 'responsibleUserIds'
| 'purgeSubmissionsAfterDays'
>
export type FormData = Record<string, string | string[]>

Expand Down
27 changes: 16 additions & 11 deletions src/modules/users/server/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,23 @@ export const getUserProviderQuery = (
provider: string,
extension: string,
address: string
) => ({
authIds: {
[Op.and]: [
{ [Op.not]: '{}' },
sequelize.literal(`jsonb_exists("User"."authIds", '${provider}')`),
sequelize.literal(`exists(
select 1 from jsonb_array_elements("User"."authIds"->'${provider}'->'${extension}') as elem
where elem->>'address' = '${address}'
) => {
const providerEsc = sequelize.escape(provider)
const extensionEsc = sequelize.escape(extension)
const addressEsc = sequelize.escape(address)
return {
authIds: {
[Op.and]: [
{ [Op.not]: '{}' },
sequelize.literal(`jsonb_exists("User"."authIds", ${providerEsc})`),
sequelize.literal(`exists(
select 1 from jsonb_array_elements("User"."authIds"->${providerEsc}->${extensionEsc}) as elem
where elem->>'address' = ${addressEsc}
)`),
],
},
})
],
},
}
}

export const removeAuthId = (
authIds: Record<string, AuthAddressPair[]>,
Expand Down
Loading

0 comments on commit efcfd58

Please sign in to comment.