@nanas-home/hub-common 0.45.1015 → 0.47.1035

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.
@@ -100,6 +100,8 @@ var apiRouteParam = {
100
100
  bookingUuid: ":bookingUuid",
101
101
  bookingAddonUuid: ":bookingAddonUuid",
102
102
  userMembershipUuid: ":userMembershipUuid",
103
+ bugReportUuid: ":bugReportUuid",
104
+ linkTypeUuid: ":linkTypeUuid",
103
105
  linkUuid: ":linkUuid",
104
106
  linkType: ":linkType",
105
107
  // Stripe
@@ -114,12 +116,19 @@ var apiRoute = {
114
116
  devJwt: "/jwt",
115
117
  login: "/login",
116
118
  signup: "/signup",
119
+ confirmEmail: `/confirm-email/${apiRouteParam.linkTypeUuid}`,
117
120
  swagger: { name: "Auth", description: "Endpoints for login, sign up etc" },
118
121
  token: {
119
122
  prefix: "/token",
120
123
  swagger: { name: "AuthToken", description: "Endpoints for creating an API token" }
121
124
  }
122
125
  },
126
+ bugReport: {
127
+ prefix: "/bug-report",
128
+ getAllForUser: "/user",
129
+ submit: "/user",
130
+ swagger: { name: "BugReports", description: "Endpoints for Users to submit BugReports" }
131
+ },
123
132
  version: "/version"
124
133
  },
125
134
  admin: {
@@ -153,6 +162,11 @@ var apiRoute = {
153
162
  removeHost: `/remove-host/${apiRouteParam.bookingUuid}`,
154
163
  swagger: { name: "Bookings", description: "Endpoints for managing bookings" }
155
164
  },
165
+ bugReport: {
166
+ prefix: "/admin/bug-report",
167
+ getForUser: `/user/${apiRouteParam.userUuid}`,
168
+ swagger: { name: "BugReports", description: "Endpoints for managing BugReports" }
169
+ },
156
170
  invoice: {
157
171
  prefix: "/invoice",
158
172
  getByUserId: `/user/${apiRouteParam.userUuid}`,
@@ -238,24 +252,6 @@ var apiRoute = {
238
252
  removeToBooking: `/${apiRouteParam.bookingAddonUuid}`,
239
253
  swagger: { name: "BookingAddons", description: "Endpoints for User to manage their Booking Addons" }
240
254
  },
241
- stripe: {
242
- invoice: {
243
- prefix: "/invoice",
244
- getByForUser: `/user`,
245
- getByBookingId: `/booking/${apiRouteParam.bookingUuid}`,
246
- getByBookingAddonId: `/booking-addon/${apiRouteParam.bookingAddonUuid}`,
247
- swagger: { name: "Invoices", description: "Endpoints for User to view the Invoices they have been issued" }
248
- },
249
- paymentMethod: {
250
- prefix: "/payment-method",
251
- getByForUser: `/user`,
252
- prepareCreateForUser: `/user/setup`,
253
- setDefaultPaymentMethod: `/default/${apiRouteParam.stripeId}`,
254
- createForUser: `/user`,
255
- deleteForUser: `/${apiRouteParam.stripeId}`,
256
- swagger: { name: "PaymentMethods", description: "Endpoints for User to view their Payment Methods" }
257
- }
258
- },
259
255
  membership: {
260
256
  prefix: "/membership",
261
257
  getAll: "",
@@ -278,6 +274,24 @@ var apiRoute = {
278
274
  getProfileById: `/${apiRouteParam.userUuid}/profile.png`,
279
275
  swagger: { name: "Profile", description: "Endpoints for User to manage their Profile" }
280
276
  },
277
+ stripe: {
278
+ invoice: {
279
+ prefix: "/invoice",
280
+ getByForUser: `/user`,
281
+ getByBookingId: `/booking/${apiRouteParam.bookingUuid}`,
282
+ getByBookingAddonId: `/booking-addon/${apiRouteParam.bookingAddonUuid}`,
283
+ swagger: { name: "Invoices", description: "Endpoints for User to view the Invoices they have been issued" }
284
+ },
285
+ paymentMethod: {
286
+ prefix: "/payment-method",
287
+ getByForUser: `/user`,
288
+ prepareCreateForUser: `/user/setup`,
289
+ setDefaultPaymentMethod: `/default/${apiRouteParam.stripeId}`,
290
+ createForUser: `/user`,
291
+ deleteForUser: `/${apiRouteParam.stripeId}`,
292
+ swagger: { name: "PaymentMethods", description: "Endpoints for User to view their Payment Methods" }
293
+ }
294
+ },
281
295
  upload: {
282
296
  prefix: "/upload",
283
297
  image: "/image",
@@ -293,6 +307,11 @@ var apiRoute = {
293
307
  cancel: `/${apiRouteParam.userMembershipUuid}/cancel`,
294
308
  bookingDays: "/booking-days",
295
309
  swagger: { name: "UserMembership", description: "Endpoints for User to get info about their Memberships" }
310
+ },
311
+ register: {
312
+ prefix: "/register",
313
+ signUpForUser: "/user",
314
+ swagger: { name: "Registration", description: "Endpoints for Users to register" }
296
315
  }
297
316
  }
298
317
  };
@@ -347,6 +366,18 @@ var BookingStatusType = /* @__PURE__ */ ((BookingStatusType2) => {
347
366
  return BookingStatusType2;
348
367
  })(BookingStatusType || {});
349
368
 
369
+ // src/contracts/generated/enum/bugReportStatusType.ts
370
+ var BugReportStatusType = /* @__PURE__ */ ((BugReportStatusType2) => {
371
+ BugReportStatusType2[BugReportStatusType2["Pending"] = 0] = "Pending";
372
+ BugReportStatusType2[BugReportStatusType2["Closed"] = 1] = "Closed";
373
+ BugReportStatusType2[BugReportStatusType2["Fixed"] = 2] = "Fixed";
374
+ BugReportStatusType2[BugReportStatusType2["InDevelopment"] = 3] = "InDevelopment";
375
+ BugReportStatusType2[BugReportStatusType2["Testing"] = 4] = "Testing";
376
+ BugReportStatusType2[BugReportStatusType2["Planned"] = 5] = "Planned";
377
+ BugReportStatusType2[BugReportStatusType2["WontFix"] = 6] = "WontFix";
378
+ return BugReportStatusType2;
379
+ })(BugReportStatusType || {});
380
+
350
381
  // src/contracts/generated/enum/commentLinkType.ts
351
382
  var CommentLinkType = /* @__PURE__ */ ((CommentLinkType2) => {
352
383
  CommentLinkType2[CommentLinkType2["Unknown"] = 0] = "Unknown";
@@ -422,6 +453,8 @@ var PermissionType = /* @__PURE__ */ ((PermissionType2) => {
422
453
  PermissionType2[PermissionType2["UserManage"] = 21] = "UserManage";
423
454
  PermissionType2[PermissionType2["UserDelete"] = 22] = "UserDelete";
424
455
  PermissionType2[PermissionType2["ActivitiesView"] = 30] = "ActivitiesView";
456
+ PermissionType2[PermissionType2["StripeView"] = 31] = "StripeView";
457
+ PermissionType2[PermissionType2["UserFlagManage"] = 32] = "UserFlagManage";
425
458
  PermissionType2[PermissionType2["UserPermissionView"] = 40] = "UserPermissionView";
426
459
  PermissionType2[PermissionType2["UserPermissionManage"] = 41] = "UserPermissionManage";
427
460
  PermissionType2[PermissionType2["UserPermissionDelete"] = 42] = "UserPermissionDelete";
@@ -455,6 +488,9 @@ var PermissionType = /* @__PURE__ */ ((PermissionType2) => {
455
488
  PermissionType2[PermissionType2["InvoiceView"] = 200] = "InvoiceView";
456
489
  PermissionType2[PermissionType2["InvoiceManage"] = 201] = "InvoiceManage";
457
490
  PermissionType2[PermissionType2["InvoiceDelete"] = 202] = "InvoiceDelete";
491
+ PermissionType2[PermissionType2["BugReportView"] = 210] = "BugReportView";
492
+ PermissionType2[PermissionType2["BugReportManage"] = 211] = "BugReportManage";
493
+ PermissionType2[PermissionType2["BugReportDelete"] = 212] = "BugReportDelete";
458
494
  return PermissionType2;
459
495
  })(PermissionType || {});
460
496
 
@@ -518,6 +554,14 @@ var SearchableColumnType = /* @__PURE__ */ ((SearchableColumnType2) => {
518
554
  return SearchableColumnType2;
519
555
  })(SearchableColumnType || {});
520
556
 
557
+ // src/contracts/generated/enum/tempLinkType.ts
558
+ var TempLinkType = /* @__PURE__ */ ((TempLinkType2) => {
559
+ TempLinkType2[TempLinkType2["Unknown"] = 0] = "Unknown";
560
+ TempLinkType2[TempLinkType2["PasswordReset"] = 1] = "PasswordReset";
561
+ TempLinkType2[TempLinkType2["EmailConfirmation"] = 2] = "EmailConfirmation";
562
+ return TempLinkType2;
563
+ })(TempLinkType || {});
564
+
521
565
  // src/contracts/generated/enum/uploadLinkType.ts
522
566
  var UploadLinkType = /* @__PURE__ */ ((UploadLinkType2) => {
523
567
  UploadLinkType2[UploadLinkType2["Unknown"] = 0] = "Unknown";
@@ -539,6 +583,8 @@ var uploadsThatNeedEncryption = [];
539
583
  var UserAccountFlagType = /* @__PURE__ */ ((UserAccountFlagType2) => {
540
584
  UserAccountFlagType2[UserAccountFlagType2["Unknown"] = 0] = "Unknown";
541
585
  UserAccountFlagType2[UserAccountFlagType2["ChangePassword"] = 1] = "ChangePassword";
586
+ UserAccountFlagType2[UserAccountFlagType2["EmailNotVerified"] = 2] = "EmailNotVerified";
587
+ UserAccountFlagType2[UserAccountFlagType2["AccountDisabled"] = 3] = "AccountDisabled";
542
588
  return UserAccountFlagType2;
543
589
  })(UserAccountFlagType || {});
544
590
 
@@ -664,6 +710,13 @@ var BookingAddonRestriction = {
664
710
  quantity: { min: 0, max: 1e3 }
665
711
  };
666
712
 
713
+ // src/contracts/generated/restrictions/bugReportRestriction.ts
714
+ var BugReportRestriction = {
715
+ comment: { maxLength: 1e3 },
716
+ metadata: { maxLength: 1e3 },
717
+ logs: { maxLength: 5e4 }
718
+ };
719
+
667
720
  // src/contracts/generated/restrictions/commentRestriction.ts
668
721
  var CommentRestriction = {
669
722
  name: { maxLength: 80 },
@@ -764,6 +817,7 @@ var searchColumns = {
764
817
  user: [{ "property": "types", "type": 1 }, { "property": "firstName", "type": 2 }, { "property": "lastName", "type": 2 }, { "property": "email", "type": 2 }, { "property": "hubspotId", "type": 2 }, { "property": "flags", "type": 1 }, { "property": "dateCreated", "type": 5 }],
765
818
  upload: [{ "property": "linkUuid", "type": 2 }, { "property": "linkType", "type": 2 }, { "property": "type", "type": 2 }, { "property": "fileName", "type": 2 }, { "property": "blobType", "type": 2 }, { "property": "sizeInKb", "type": 2 }, { "property": "dateCreated", "type": 5 }],
766
819
  booking: [{ "property": "startDate", "type": 5 }, { "property": "endDate", "type": 5 }, { "property": "status", "type": 2 }, { "property": "notes", "type": 2 }],
820
+ bugReport: [{ "property": "status", "type": 0 }, { "property": "comment", "type": 2 }, { "property": "metadata", "type": 2 }, { "property": "logs", "type": 2 }],
767
821
  question: [{ "property": "forTypes", "type": 1 }, { "property": "transKey", "type": 2 }, { "property": "fallback", "type": 2 }, { "property": "category", "type": 2 }, { "property": "type", "type": 0 }, { "property": "sortOrder", "type": 3 }, { "property": "visible", "type": 4 }, { "property": "notes", "type": 2 }]
768
822
  };
769
823
 
@@ -931,12 +985,13 @@ ${(log.optionalParams ?? []).join("\n\r")}`;
931
985
  logFunc(`${dateString}: ${messageString}`, logStyle);
932
986
  };
933
987
  _track = (type, groups) => (message, ...optionalParams) => {
988
+ const localOptionalParams = (optionalParams?.length ?? 0) < 1 ? void 0 : optionalParams.map((op) => JSON.stringify(op));
934
989
  const log = {
935
990
  type,
936
991
  groups,
937
992
  message,
938
993
  date: /* @__PURE__ */ new Date(),
939
- optionalParams: optionalParams.map((op) => JSON.stringify(op, null, 2))
994
+ optionalParams: localOptionalParams
940
995
  };
941
996
  this.logs = this.logs.filter((t) => t.date > this._latestLogDate);
942
997
  this.logs.push(log);
@@ -1660,6 +1715,33 @@ var maxDate = (maxDate2) => (value) => {
1660
1715
  };
1661
1716
  };
1662
1717
 
1718
+ // src/validation/emailValidation.ts
1719
+ var emailValid = (value) => {
1720
+ if (value.includes("@") == false) {
1721
+ return {
1722
+ isValid: false,
1723
+ errorMessageTransKey: "email_at_sign_validator",
1724
+ errorMessage: `Email should have an @ sign`
1725
+ };
1726
+ }
1727
+ const segments = value.split("@");
1728
+ if (segments.length > 2) {
1729
+ return {
1730
+ isValid: false,
1731
+ errorMessageTransKey: "email_too_many_at_signs_validator",
1732
+ errorMessage: `Email contains too many @ signs`
1733
+ };
1734
+ }
1735
+ if ((segments[1] ?? "").includes(".") == false) {
1736
+ return {
1737
+ isValid: false,
1738
+ errorMessageTransKey: "email_should_have_a_period_after_at_sign_validator",
1739
+ errorMessage: `Email is not valid`
1740
+ };
1741
+ }
1742
+ return { isValid: true };
1743
+ };
1744
+
1663
1745
  // src/validation/numberValidation.ts
1664
1746
  var isNumber = (value) => {
1665
1747
  if (value != null) {
@@ -1696,6 +1778,26 @@ var maxValue = (max) => (value) => {
1696
1778
  };
1697
1779
  };
1698
1780
 
1781
+ // src/validation/passwordValidation.ts
1782
+ var specialChars = "<>@!#$%^&*()_+[]{}?:;|'\"\\,./~`-=";
1783
+ var capitalizeAlphabet2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1784
+ var errorMessage = `Password should be minimum of 8 characters, with an uppercase letter, at least 1 number and 1 special character`;
1785
+ var passwordValid = (value) => {
1786
+ const passwordCharArr = value.split("");
1787
+ const defaultValidation = { isValid: false, errorMessage };
1788
+ if (value.length < UserRestriction.password.minLength)
1789
+ return { ...defaultValidation, errorMessageTransKey: "password_too_short_validator" };
1790
+ const hasNumber = passwordCharArr.filter((c) => isNaN(Number(c)) == false).length > 0;
1791
+ if (hasNumber == false) return { ...defaultValidation, errorMessageTransKey: "password_missing_number_validator" };
1792
+ const hasSpecialChar = arrayContains(passwordCharArr, specialChars.split(""), "OR");
1793
+ if (hasSpecialChar == false)
1794
+ return { ...defaultValidation, errorMessageTransKey: "password_missing_special_char_validator" };
1795
+ const hasUppercase = arrayContains(passwordCharArr, capitalizeAlphabet2.split(""), "OR");
1796
+ if (hasUppercase == false)
1797
+ return { ...defaultValidation, errorMessageTransKey: "password_missing_uppercase_validator" };
1798
+ return { isValid: true };
1799
+ };
1800
+
1699
1801
  // src/validation/textValidation.ts
1700
1802
  var minLength = (minLengthVal) => (value) => {
1701
1803
  if ((value?.length ?? 0) >= minLengthVal) {
@@ -1753,4 +1855,4 @@ var shouldBeYoutubeUrl = (value) => {
1753
1855
  };
1754
1856
  };
1755
1857
 
1756
- export { APP_TYPE_TOKEN, ActivityRestriction, ActivityType, AddressLinkType, AddressRestriction, AnswerLinkType, AnswerRestriction, AppType, BOT_PATH_TOKEN, BookingAddonRestriction, BookingAddonType, BookingRestriction, BookingStatusType, CommentLinkType, CommentRestriction, CommonConfigService, DependencyInjectionContainer, DrivingRouteRestriction, InvoiceRestriction, InvoiceStatusType, LogService, MembershipBillingCycleType, MembershipRestriction, MembershipStatus, MembershipType, MouseButton, NetworkState, OrderDirectionType, PermissionRestriction, PermissionType, PetRestriction, PetSexType, PetStatusType, PetType, QuestionForType, QuestionRestriction, QuestionType, SITE_CONFIG_TOKEN, SearchableColumnType, StripeRestriction, TranslationService, UnavailabilityRestriction, UploadLinkType, UploadRestriction, UploadType, UserAccountFlagType, UserRestriction, UserType, addDays, addMinutes, addMonths, addSeconds, addSpacesForEnum, addToParallelTasks, anyObject, apiRoute, apiRouteParam, arrayContains, arrayOfNLength, capitalizeFirstLetter, changeMonth, colourPalette, convertToDate, convertToFullDateSelection, createToken, cyrb53, dateDiffInDays, debounceLeading, defaultSiteColour, fakePromise, formatDate, formatFileSize, formatForBookingDate, formatForDateDropdown, formatForDateLocal, formatForDateLocalDetailed, formatForDateOfBirth, formatForDateWithTime, getActiveUserMembership, getAgeInYears, getAllPetQuestionForType, getAppType, getArrFromEnum, getBotPath, getCalendarDropdownDisplayValue, getCalendarDropdownForDateOfBirthDisplayValue, getCommonConfig, getDayClassObject, getDayElements, getDayHeadingElements, getImageParams, getLog, getMimeTypeFromExtension, getPayloadFromJwt, getPerformanceTimer, getPetAvatarUrl, getSiteColour, getSiteConfig, getUserAvatarUrl, getUsersName, getWeekNumber, hasRequiredPermissions, hubspotQuestionsExport, isBefore, isDateAfterStart, isDateBeforeEnd, isDateDisabledByDisabledDays, isDateEnabledByEnabledDays, isNumber, isSameDay, isWebApp, lowercaseFirstLetter, makeArrayOrDefault, maxDate, maxItems, maxLength, maxValue, mimeTypeLookup, minDate, minItems, minLength, minUrlLength, minValue, monthTranslationKeyOrder, multiValidation, nameof, nanasFonts, noValidation, notNull, onlyUnique, portalGlyphLength, promiseFromValue, randomIntFromRange, randomItemFromArray, rootContainer, rootDependencyInjectionSetup, searchColumns, selectedItemsExist, selectedOptionIsInEnum, separateValidation, setContainerToken, setContainerTokenLazy, setSiteConfig, shouldBeUrl, shouldBeYoutubeUrl, showSecondCalendar, socialLinks, timeout, tryParseNumber, uploadsThatNeedEncryption, urlRef, uuidv4, validUuidChars, validateForEach, webAppTypes, weekdayTranslationKeyOrder };
1858
+ export { APP_TYPE_TOKEN, ActivityRestriction, ActivityType, AddressLinkType, AddressRestriction, AnswerLinkType, AnswerRestriction, AppType, BOT_PATH_TOKEN, BookingAddonRestriction, BookingAddonType, BookingRestriction, BookingStatusType, BugReportRestriction, BugReportStatusType, CommentLinkType, CommentRestriction, CommonConfigService, DependencyInjectionContainer, DrivingRouteRestriction, InvoiceRestriction, InvoiceStatusType, LogService, MembershipBillingCycleType, MembershipRestriction, MembershipStatus, MembershipType, MouseButton, NetworkState, OrderDirectionType, PermissionRestriction, PermissionType, PetRestriction, PetSexType, PetStatusType, PetType, QuestionForType, QuestionRestriction, QuestionType, SITE_CONFIG_TOKEN, SearchableColumnType, StripeRestriction, TempLinkType, TranslationService, UnavailabilityRestriction, UploadLinkType, UploadRestriction, UploadType, UserAccountFlagType, UserRestriction, UserType, addDays, addMinutes, addMonths, addSeconds, addSpacesForEnum, addToParallelTasks, anyObject, apiRoute, apiRouteParam, arrayContains, arrayOfNLength, capitalizeFirstLetter, changeMonth, colourPalette, convertToDate, convertToFullDateSelection, createToken, cyrb53, dateDiffInDays, debounceLeading, defaultSiteColour, emailValid, fakePromise, formatDate, formatFileSize, formatForBookingDate, formatForDateDropdown, formatForDateLocal, formatForDateLocalDetailed, formatForDateOfBirth, formatForDateWithTime, getActiveUserMembership, getAgeInYears, getAllPetQuestionForType, getAppType, getArrFromEnum, getBotPath, getCalendarDropdownDisplayValue, getCalendarDropdownForDateOfBirthDisplayValue, getCommonConfig, getDayClassObject, getDayElements, getDayHeadingElements, getImageParams, getLog, getMimeTypeFromExtension, getPayloadFromJwt, getPerformanceTimer, getPetAvatarUrl, getSiteColour, getSiteConfig, getUserAvatarUrl, getUsersName, getWeekNumber, hasRequiredPermissions, hubspotQuestionsExport, isBefore, isDateAfterStart, isDateBeforeEnd, isDateDisabledByDisabledDays, isDateEnabledByEnabledDays, isNumber, isSameDay, isWebApp, lowercaseFirstLetter, makeArrayOrDefault, maxDate, maxItems, maxLength, maxValue, mimeTypeLookup, minDate, minItems, minLength, minUrlLength, minValue, monthTranslationKeyOrder, multiValidation, nameof, nanasFonts, noValidation, notNull, onlyUnique, passwordValid, portalGlyphLength, promiseFromValue, randomIntFromRange, randomItemFromArray, rootContainer, rootDependencyInjectionSetup, searchColumns, selectedItemsExist, selectedOptionIsInEnum, separateValidation, setContainerToken, setContainerTokenLazy, setSiteConfig, shouldBeUrl, shouldBeYoutubeUrl, showSecondCalendar, socialLinks, timeout, tryParseNumber, uploadsThatNeedEncryption, urlRef, uuidv4, validUuidChars, validateForEach, webAppTypes, weekdayTranslationKeyOrder };
@@ -1,7 +1,11 @@
1
1
  import requireDebugMarker from './rules/require-debug-marker.js';
2
+ import requireDraggableAttr from './rules/require-draggable-attr.js';
3
+ import preventParentImports from './rules/prevent-parent-imports.js';
2
4
 
3
5
  export default {
4
6
  rules: {
5
7
  'require-debug-marker': requireDebugMarker,
8
+ 'require-draggable-attr': requireDraggableAttr,
9
+ 'prevent-parent-imports': preventParentImports,
6
10
  },
7
11
  };
@@ -0,0 +1,43 @@
1
+ export default {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description: 'Disallow parent-directory imports (../)',
6
+ },
7
+ messages: {
8
+ noParent: "Parent directory imports ('../') are not allowed.",
9
+ },
10
+ schema: [],
11
+ },
12
+
13
+ create(context) {
14
+ return {
15
+ ImportDeclaration(node) {
16
+ const value = node.source.value;
17
+
18
+ if (typeof value === 'string') {
19
+ if (value.startsWith('./') || value.startsWith('../')) {
20
+ context.report({
21
+ node,
22
+ messageId: 'noParent',
23
+ });
24
+ }
25
+ }
26
+ },
27
+
28
+ CallExpression(node) {
29
+ // Handle require("../something")
30
+ if (node.callee.name === 'require' && node.arguments.length && node.arguments[0].type === 'Literal') {
31
+ const value = node.arguments[0].value;
32
+
33
+ if (typeof value === 'string' && value.startsWith('../')) {
34
+ context.report({
35
+ node,
36
+ messageId: 'noParent',
37
+ });
38
+ }
39
+ }
40
+ },
41
+ };
42
+ },
43
+ };
@@ -0,0 +1,50 @@
1
+ export default {
2
+ meta: {
3
+ type: 'problem',
4
+ docs: {
5
+ description: 'Ensure draggable attribute is defined on <img> and <a> elements',
6
+ },
7
+ schema: [],
8
+ messages: {
9
+ missingDraggable: "The '{{tag}}' element must have a 'draggable' attribute defined.",
10
+ },
11
+ },
12
+
13
+ create(context) {
14
+ return {
15
+ JSXOpeningElement(node) {
16
+ const filename = context.filename;
17
+ if (filename.endsWith('.ts')) return {};
18
+ if (filename.endsWith('.meta.tsx')) return {};
19
+
20
+ const tagName = node.name.name;
21
+
22
+ // Only check <img> and <a>
23
+ if (tagName !== 'img' && tagName !== 'a') return;
24
+
25
+ const draggableAttr = node.attributes.find(
26
+ (attr) => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'draggable',
27
+ );
28
+
29
+ // Missing attribute entirely
30
+ if (!draggableAttr) {
31
+ context.report({
32
+ node,
33
+ messageId: 'missingDraggable',
34
+ data: { tag: tagName },
35
+ });
36
+ return;
37
+ }
38
+
39
+ // Attribute exists but has no value: <img draggable>
40
+ if (!draggableAttr.value) {
41
+ context.report({
42
+ node,
43
+ messageId: 'missingDraggable',
44
+ data: { tag: tagName },
45
+ });
46
+ }
47
+ },
48
+ };
49
+ },
50
+ };