From 9566106433e4eddbcd9f206bc012e20338dd4f59 Mon Sep 17 00:00:00 2001 From: Gavin Barron Date: Fri, 9 Dec 2022 15:11:27 -0800 Subject: [PATCH] feat: typed events Changes the package use to generate the custom-elements.json file used to build React components and tell Storybook about the web-components definitions. --- package.json | 4 +- .../mgt-people-picker/mgt-people-picker.ts | 2 +- .../src/components/mgt-tasks/mgt-tasks.ts | 2 +- .../src/components/templatedComponent.ts | 13 +- packages/mgt-react/package.json | 3 +- packages/mgt-react/scripts/generate.js | 93 +++++++---- packages/mgt-react/src/generated/react.ts | 145 +++++++++--------- 7 files changed, 153 insertions(+), 109 deletions(-) diff --git a/package.json b/package.json index 486cdc2e50..e3d0a26579 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "samples/teamsfx-app/tabs" ], "scripts": { + "analyze": "custom-elements-manifest analyze --litelement --globs \"./packages/*/src/**/*.ts\"", "build": "npm run prettier:check && npm run clean && lerna run build --scope @microsoft/*", "build:compile": "npm run prettier:check && npm run clean && lerna run build:compile --scope @microsoft/*", "build:mgt": "cd ./packages/mgt && npm run build", @@ -39,7 +40,7 @@ "prettier:check": "npm run prettier:base -- --check \"packages/**/*.{ts,tsx}\"", "prettier:write": "npm run prettier:base -- --write \"packages/**/*.{ts,tsx}\"", "storybook": "start-storybook -p 6006 -s assets", - "storybook:dev": "npm run build:compile && wca analyze packages --format json --outFile custom-elements.json", + "storybook:dev": "npm run build:compile && npm run analyze", "storybook:watch": "npm-run-all --parallel watch storybook:bundle:watch storybook", "storybook:bundle": "rollup -c ./.storybook/rollup.config.js", "storybook:bundle:watch": "rollup -c ./.storybook/rollup.config.js --watch", @@ -61,6 +62,7 @@ "@babel/preset-env": "^7.12.7", "@babel/preset-react": "^7.12.7", "@babel/preset-typescript": "^7.12.7", + "@custom-elements-manifest/analyzer": "^0.6.6", "@octokit/rest": "^18.5.3", "@storybook/addon-a11y": "^6.4.4", "@storybook/addon-actions": "^6.4.4", diff --git a/packages/mgt-components/src/components/mgt-people-picker/mgt-people-picker.ts b/packages/mgt-components/src/components/mgt-people-picker/mgt-people-picker.ts index e35416eb89..be9bd29074 100644 --- a/packages/mgt-components/src/components/mgt-people-picker/mgt-people-picker.ts +++ b/packages/mgt-components/src/components/mgt-people-picker/mgt-people-picker.ts @@ -58,7 +58,7 @@ interface IFocusable { * @class MgtPicker * @extends {MgtTemplatedComponent} * - * @fires selectionChanged - Fired when selection changes + * @fires {CustomEvent} selectionChanged - Fired when set of selected people changes * * @cssprop --color - {Color} Default font color * diff --git a/packages/mgt-components/src/components/mgt-tasks/mgt-tasks.ts b/packages/mgt-components/src/components/mgt-tasks/mgt-tasks.ts index 93c1b1c893..a25893e620 100644 --- a/packages/mgt-components/src/components/mgt-tasks/mgt-tasks.ts +++ b/packages/mgt-components/src/components/mgt-tasks/mgt-tasks.ts @@ -215,7 +215,7 @@ export class MgtTasks extends MgtTemplatedComponent { * * @memberof MgtTasks */ - public get isNewTaskVisible() { + public get isNewTaskVisible(): boolean { return this._isNewTaskVisible; } diff --git a/packages/mgt-element/src/components/templatedComponent.ts b/packages/mgt-element/src/components/templatedComponent.ts index 9f4d32c2ca..7c0fb1f2d0 100644 --- a/packages/mgt-element/src/components/templatedComponent.ts +++ b/packages/mgt-element/src/components/templatedComponent.ts @@ -28,6 +28,14 @@ interface RenderedTemplates { }; } +// tslint:disable: completed-docs +export interface TemplateRenderedData { + templateType: string; + context: Record; + element: HTMLElement; +} +// tslint:enable: completed-docs + /** * An abstract class that defines a templatable web component * @@ -36,7 +44,7 @@ interface RenderedTemplates { * @class MgtTemplatedComponent * @extends {MgtBaseComponent} * - * @fires templateRendered - fires when a template is rendered + * @fires {CustomEvent} templateRendered - fires when a template is rendered */ export abstract class MgtTemplatedComponent extends MgtBaseComponent { /** @@ -134,7 +142,8 @@ export abstract class MgtTemplatedComponent extends MgtBaseComponent { this._renderedTemplates[slotName] = { context: dataContext, slot: div }; - this.fireCustomEvent('templateRendered', { templateType, context: dataContext, element: div }); + const templateRenderedData: TemplateRenderedData = { templateType, context: dataContext, element: div }; + this.fireCustomEvent('templateRendered', templateRenderedData); return template; } diff --git a/packages/mgt-react/package.json b/packages/mgt-react/package.json index 017e32f47b..043615b148 100644 --- a/packages/mgt-react/package.json +++ b/packages/mgt-react/package.json @@ -28,7 +28,8 @@ "build": "npm run clean && npm run generate && tsc", "clean": "node ./scripts/clean.js", "postpack": "cpx *.tgz ../../artifacts", - "generate": "wca analyze ../mgt-components/src --format json --outFile temp/web-components.json && node ./scripts/generate.js" + "analyze": "custom-elements-manifest analyze --litelement --globs \"../*/src/**/*.ts\" --outdir temp", + "generate": "npm run analyze && node ./scripts/generate.js" }, "dependencies": { "@microsoft/mgt-components": "*", diff --git a/packages/mgt-react/scripts/generate.js b/packages/mgt-react/scripts/generate.js index f597131b11..dd2a957679 100644 --- a/packages/mgt-react/scripts/generate.js +++ b/packages/mgt-react/scripts/generate.js @@ -1,8 +1,8 @@ var fs = require('fs-extra'); -let wc = JSON.parse(fs.readFileSync(`${__dirname}/../temp/web-components.json`)); +let wc = JSON.parse(fs.readFileSync(`${__dirname}/../temp/custom-elements.json`)); -const primitives = new Set(['string', 'boolean', 'number', 'any']); +const primitives = new Set(['string', 'boolean', 'number', 'any', 'void', 'null', 'undefined']); const mgtComponentImports = new Set(); const mgtElementImports = new Set(); @@ -25,30 +25,65 @@ let output = ''; const wrappers = []; -for (const tag of wc.tags) { - if (!tags.has(tag.name)) { - continue; +const customTags = []; +for (const module of wc.modules) { + for (const d of module.declarations) { + if (d.customElement && d.tagName && tags.has(d.tagName)) { + customTags.push(d); + } + } +} + +const removeGenericTypeDecoration = type => { + if (type.endsWith('[]')) { + return type.substring(0, type.length - 2); + } else if (type.startsWith('Array<')) { + return removeGenericTypeDecoration(type.substring(6, type.length - 1)); + } else if (type.startsWith('CustomEvent<')) { + return removeGenericTypeDecoration(type.substring(12, type.length - 1)); + } + return type; +}; + +const addTypeToImports = type => { + if (type === '*') { + return; } - const className = tag.name + for (let t of type.split('|')) { + t = removeGenericTypeDecoration(t.trim()); + if (t.startsWith('MicrosoftGraph.') || t.startsWith('MicrosoftGraphBeta.')) { + return; + } + + if (t.startsWith('MgtElement.') && !mgtElementImports.has(t)) { + mgtElementImports.add(t.split('.')[1]); + } else if (!primitives.has(t) && !mgtComponentImports.has(t)) { + mgtComponentImports.add(t); + } + } +}; + +for (const tag of customTags.sort((a, b) => (a.tagName > b.tagName ? 1 : -1))) { + const className = tag.tagName .split('-') .slice(1) .map(t => t[0].toUpperCase() + t.substring(1)) .join(''); wrappers.push({ - tag: tag.name, + tag: tag.tagName, propsType: className + 'Props', className: className }); const props = {}; - for (let i = 0; i < tag.properties.length; ++i) { - const prop = tag.properties[i]; - let type = prop.type; + for (let i = 0; i < tag.members.length; ++i) { + const prop = tag.members[i]; + let type = prop.type?.text; - if (type) { + if (type && prop.kind === 'field' && prop.privacy === 'public' && !prop.static) { if (prop.name) { props[prop.name] = type; } @@ -56,30 +91,16 @@ for (const tag of wc.tags) { if (type.includes('|')) { const types = type.split('|'); for (const t of types) { - tag.properties.push({ - type: t.trim() + tag.members.push({ + kind: 'field', + privacy: 'public', + type: { text: t.trim() } }); } continue; } - if (type.endsWith('[]')) { - type = type.substring(0, type.length - 2); - } else if (type.startsWith('Array<')) { - type = type.substring(6, type.length - 1); - } else if (type === '*') { - continue; - } - - if (type.startsWith('MicrosoftGraph.') || type.startsWith('MicrosoftGraphBeta.')) { - continue; - } - - if (type.startsWith('MgtElement.') && !mgtElementImports.has(type)) { - mgtElementImports.add(type.split('.')[1]); - } else if (!primitives.has(type) && !mgtComponentImports.has(type)) { - mgtComponentImports.add(type); - } + addTypeToImports(type); } } @@ -95,7 +116,17 @@ for (const tag of wc.tags) { if (tag.events) { for (const event of tag.events) { - propsType += `\t${event.name}?: (e: Event) => void;\n`; + if (event.type && event.type.text) { + // remove MgtElement. prefix as this it only used to ensure it's imported from the correct package + propsType += `\t${event.name}?: (e: ${event.type.text.replace( + 'CustomEvent void;\n`; + // also ensure that the necessary import is added to either mgt-element or mgt-component imports + addTypeToImports(event.type.text); + } else { + propsType += `\t${event.name}?: (e: Event) => void;\n`; + } } } diff --git a/packages/mgt-react/src/generated/react.ts b/packages/mgt-react/src/generated/react.ts index 0d87469507..300d4ff130 100644 --- a/packages/mgt-react/src/generated/react.ts +++ b/packages/mgt-react/src/generated/react.ts @@ -1,4 +1,4 @@ -import { OfficeGraphInsightString,ViewType,ResponseType,IDynamicPerson,PersonType,GroupType,UserType,PersonCardInteraction,MgtPersonConfig,AvatarSize,PersonViewType,TasksStringResource,TasksSource,TaskFilter,SelectedChannel,TodoFilter } from '@microsoft/mgt-components'; +import { OfficeGraphInsightString,ViewType,ResponseType,IDynamicPerson,PersonCardInteraction,PersonType,GroupType,UserType,AvatarSize,PersonViewType,TasksStringResource,TasksSource,TaskFilter,SelectedChannel,TodoFilter } from '@microsoft/mgt-components'; import { TemplateContext,ComponentMediaQuery } from '@microsoft/mgt-element'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import * as MicrosoftGraphBeta from '@microsoft/microsoft-graph-types-beta'; @@ -19,31 +19,6 @@ export type AgendaProps = { templateRendered?: (e: Event) => void; } -export type FileListProps = { - fileListQuery?: string; - fileQueries?: string[]; - files?: MicrosoftGraph.DriveItem[]; - siteId?: string; - driveId?: string; - groupId?: string; - itemId?: string; - itemPath?: string; - userId?: string; - insightType?: OfficeGraphInsightString; - fileExtensions?: string[]; - hideMoreFilesButton?: boolean; - maxFileSize?: number; - excludedFileExtensions?: string[]; - pageSize?: number; - itemView?: ViewType; - maxUploadFile?: number; - enableFileUpload?: boolean; - templateContext?: TemplateContext; - mediaQuery?: ComponentMediaQuery; - itemClick?: (e: Event) => void; - templateRendered?: (e: Event) => void; -} - export type FileProps = { fileQuery?: string; siteId?: string; @@ -67,6 +42,31 @@ export type FileProps = { templateRendered?: (e: Event) => void; } +export type FileListProps = { + fileListQuery?: string; + fileQueries?: string[]; + files?: MicrosoftGraph.DriveItem[]; + siteId?: string; + driveId?: string; + groupId?: string; + itemId?: string; + itemPath?: string; + userId?: string; + insightType?: OfficeGraphInsightString; + itemView?: ViewType; + fileExtensions?: string[]; + pageSize?: number; + hideMoreFilesButton?: boolean; + maxFileSize?: number; + enableFileUpload?: boolean; + maxUploadFile?: number; + excludedFileExtensions?: string[]; + templateContext?: TemplateContext; + mediaQuery?: ComponentMediaQuery; + itemClick?: (e: Event) => void; + templateRendered?: (e: Event) => void; +} + export type GetProps = { resource?: string; scopes?: string[]; @@ -76,6 +76,8 @@ export type GetProps = { pollingRate?: number; cacheEnabled?: boolean; cacheInvalidationPeriod?: number; + response?: any; + error?: any; templateContext?: TemplateContext; mediaQuery?: ComponentMediaQuery; dataChange?: (e: Event) => void; @@ -94,6 +96,23 @@ export type LoginProps = { templateRendered?: (e: Event) => void; } +export type PeopleProps = { + groupId?: string; + userIds?: string[]; + people?: IDynamicPerson[]; + peopleQueries?: string[]; + showMax?: number; + showPresence?: boolean; + personCardInteraction?: PersonCardInteraction; + resource?: string; + version?: string; + scopes?: string[]; + fallbackDetails?: IDynamicPerson[]; + templateContext?: TemplateContext; + mediaQuery?: ComponentMediaQuery; + templateRendered?: (e: Event) => void; +} + export type PeoplePickerProps = { groupId?: string; groupIds?: string[]; @@ -102,40 +121,48 @@ export type PeoplePickerProps = { userType?: UserType; transitiveSearch?: boolean; people?: IDynamicPerson[]; + showMax?: number; + disableImages?: boolean; selectedPeople?: IDynamicPerson[]; defaultSelectedUserIds?: string[]; defaultSelectedGroupIds?: string[]; placeholder?: string; + disabled?: boolean; + allowAnyEmail?: boolean; selectionMode?: string; userIds?: string[]; userFilters?: string; peopleFilters?: string; groupFilters?: string; ariaLabel?: string; - showMax?: number; - disableImages?: boolean; - disabled?: boolean; - allowAnyEmail?: boolean; templateContext?: TemplateContext; mediaQuery?: ComponentMediaQuery; - selectionChanged?: (e: Event) => void; + selectionChanged?: (e: CustomEvent) => void; templateRendered?: (e: Event) => void; } -export type PeopleProps = { - groupId?: string; - userIds?: string[]; - people?: IDynamicPerson[]; - peopleQueries?: string[]; +export type PersonProps = { + personQuery?: string; + fallbackDetails?: IDynamicPerson; + userId?: string; showPresence?: boolean; + avatarSize?: AvatarSize; + personDetails?: IDynamicPerson; + personImage?: string; + fetchImage?: boolean; + disableImageFetch?: boolean; + avatarType?: string; + personPresence?: MicrosoftGraph.Presence; personCardInteraction?: PersonCardInteraction; - resource?: string; - version?: string; - scopes?: string[]; - fallbackDetails?: IDynamicPerson[]; - showMax?: number; + line1Property?: string; + line2Property?: string; + line3Property?: string; + view?: ViewType | PersonViewType; templateContext?: TemplateContext; mediaQuery?: ComponentMediaQuery; + line1clicked?: (e: Event) => void; + line2clicked?: (e: Event) => void; + line3clicked?: (e: Event) => void; templateRendered?: (e: Event) => void; } @@ -156,32 +183,6 @@ export type PersonCardProps = { templateRendered?: (e: Event) => void; } -export type PersonProps = { - config?: MgtPersonConfig; - personQuery?: string; - fallbackDetails?: IDynamicPerson; - userId?: string; - showPresence?: boolean; - personDetails?: IDynamicPerson; - personImage?: string; - fetchImage?: boolean; - avatarType?: string; - personPresence?: MicrosoftGraph.Presence; - personCardInteraction?: PersonCardInteraction; - line1Property?: string; - line2Property?: string; - line3Property?: string; - view?: ViewType | PersonViewType; - avatarSize?: AvatarSize; - disableImageFetch?: boolean; - templateContext?: TemplateContext; - mediaQuery?: ComponentMediaQuery; - line1clicked?: (e: Event) => void; - line2clicked?: (e: Event) => void; - line3clicked?: (e: Event) => void; - templateRendered?: (e: Event) => void; -} - export type TasksProps = { res?: TasksStringResource; isNewTaskVisible?: boolean; @@ -226,22 +227,22 @@ export type TodoProps = { export const Agenda = wrapMgt('mgt-agenda'); -export const FileList = wrapMgt('mgt-file-list'); - export const File = wrapMgt('mgt-file'); +export const FileList = wrapMgt('mgt-file-list'); + export const Get = wrapMgt('mgt-get'); export const Login = wrapMgt('mgt-login'); -export const PeoplePicker = wrapMgt('mgt-people-picker'); - export const People = wrapMgt('mgt-people'); -export const PersonCard = wrapMgt('mgt-person-card'); +export const PeoplePicker = wrapMgt('mgt-people-picker'); export const Person = wrapMgt('mgt-person'); +export const PersonCard = wrapMgt('mgt-person-card'); + export const Tasks = wrapMgt('mgt-tasks'); export const TeamsChannelPicker = wrapMgt('mgt-teams-channel-picker');